/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails react-core
 */

'use strict';

import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
import {Readable} from 'stream';

// Polyfills for test environment
global.ReadableStream =
  require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

let act;
let use;
let clientExports;
let clientExportsESM;
let clientModuleError;
let webpackMap;
let Stream;
let FlightReact;
let React;
let FlightReactDOM;
let ReactDOMClient;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let ReactDOMFizzServer;
let ReactDOMStaticServer;
let Suspense;
let ErrorBoundary;
let JSDOM;
let ReactServerScheduler;
let reactServerAct;
let assertConsoleErrorDev;

describe('ReactFlightDOM', () => {
  beforeEach(() => {
    // For this first reset we are going to load the dom-node version of react-server-dom-webpack/server
    // This can be thought of as essentially being the React Server Components scope with react-server
    // condition
    jest.resetModules();

    JSDOM = require('jsdom').JSDOM;

    ReactServerScheduler = require('scheduler');
    patchSetImmediate(ReactServerScheduler);
    reactServerAct = require('internal-test-utils').act;

    // Simulate the condition resolution
    jest.mock('react', () => require('react/react.react-server'));
    FlightReact = require('react');
    FlightReactDOM = require('react-dom');

    jest.mock('react-server-dom-webpack/server', () =>
      require('react-server-dom-webpack/server.node.unbundled'),
    );
    if (__EXPERIMENTAL__) {
      jest.mock('react-server-dom-webpack/static', () =>
        require('react-server-dom-webpack/static.node.unbundled'),
      );
    }
    const WebpackMock = require('./utils/WebpackMock');
    clientExports = WebpackMock.clientExports;
    clientExportsESM = WebpackMock.clientExportsESM;
    clientModuleError = WebpackMock.clientModuleError;
    webpackMap = WebpackMock.webpackMap;

    ReactServerDOMServer = require('react-server-dom-webpack/server');
    if (__EXPERIMENTAL__) {
      ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
    }

    // This reset is to load modules for the SSR/Browser scope.
    jest.unmock('react-server-dom-webpack/server');
    __unmockReact();
    jest.resetModules();
    act = require('internal-test-utils').act;
    assertConsoleErrorDev =
      require('internal-test-utils').assertConsoleErrorDev;
    Stream = require('stream');
    React = require('react');
    use = React.use;
    Suspense = React.Suspense;
    ReactDOMClient = require('react-dom/client');
    ReactDOMFizzServer = require('react-dom/server.node');
    ReactDOMStaticServer = require('react-dom/static.node');
    ReactServerDOMClient = require('react-server-dom-webpack/client');

    ErrorBoundary = class extends React.Component {
      state = {hasError: false, error: null};
      static getDerivedStateFromError(error) {
        return {
          hasError: true,
          error,
        };
      }
      render() {
        if (this.state.hasError) {
          return this.props.fallback(this.state.error);
        }
        return this.props.children;
      }
    };
  });

  async function serverAct(callback) {
    let maybePromise;
    await reactServerAct(() => {
      maybePromise = callback();
      if (maybePromise && typeof maybePromise.catch === 'function') {
        maybePromise.catch(() => {});
      }
    });
    return maybePromise;
  }

  async function readInto(
    container: Document | HTMLElement,
    stream: ReadableStream,
  ) {
    const reader = stream.getReader();
    const decoder = new TextDecoder();
    let content = '';
    while (true) {
      const {done, value} = await reader.read();
      if (done) {
        content += decoder.decode();
        break;
      }
      content += decoder.decode(value, {stream: true});
    }
    if (container.nodeType === 9 /* DOCUMENT */) {
      const doc = new JSDOM(content).window.document;
      container.documentElement.innerHTML = doc.documentElement.innerHTML;
      while (container.documentElement.attributes.length > 0) {
        container.documentElement.removeAttribute(
          container.documentElement.attributes[0].name,
        );
      }
      const attrs = doc.documentElement.attributes;
      for (let i = 0; i < attrs.length; i++) {
        container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
      }
    } else {
      container.innerHTML = content;
    }
  }

  function getTestStream() {
    const writable = new Stream.PassThrough();
    const readable = new ReadableStream({
      start(controller) {
        writable.on('data', chunk => {
          controller.enqueue(chunk);
        });
        writable.on('end', () => {
          controller.close();
        });
      },
    });
    return {
      readable,
      writable,
    };
  }

  const theInfinitePromise = new Promise(() => {});
  function InfiniteSuspend() {
    throw theInfinitePromise;
  }

  function getMeaningfulChildren(element) {
    const children = [];
    let node = element.firstChild;
    while (node) {
      if (node.nodeType === 1) {
        if (
          // some tags are ambiguous and might be hidden because they look like non-meaningful children
          // so we have a global override where if this data attribute is included we also include the node
          node.hasAttribute('data-meaningful') ||
          (node.tagName === 'SCRIPT' &&
            node.hasAttribute('src') &&
            node.hasAttribute('async')) ||
          (node.tagName !== 'SCRIPT' &&
            node.tagName !== 'TEMPLATE' &&
            node.tagName !== 'template' &&
            !node.hasAttribute('hidden') &&
            !node.hasAttribute('aria-hidden'))
        ) {
          const props = {};
          const attributes = node.attributes;
          for (let i = 0; i < attributes.length; i++) {
            if (
              attributes[i].name === 'id' &&
              attributes[i].value.includes(':')
            ) {
              // We assume this is a React added ID that's a non-visual implementation detail.
              continue;
            }
            props[attributes[i].name] = attributes[i].value;
          }
          props.children = getMeaningfulChildren(node);
          children.push(React.createElement(node.tagName.toLowerCase(), props));
        }
      } else if (node.nodeType === 3) {
        children.push(node.data);
      }
      node = node.nextSibling;
    }
    return children.length === 0
      ? undefined
      : children.length === 1
        ? children[0]
        : children;
  }

  it('should resolve HTML using Node streams', async () => {
    function Text({children}) {
      return <span>{children}</span>;
    }
    function HTML() {
      return (
        <div>
          <Text>hello</Text>
          <Text>world</Text>
        </div>
      );
    }

    function App() {
      const model = {
        html: <HTML />,
      };
      return model;
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);
    const model = await response;
    expect(model).toEqual({
      html: (
        <div>
          <span>hello</span>
          <span>world</span>
        </div>
      ),
    });
  });

  it('should resolve the root', async () => {
    // Model
    function Text({children}) {
      return <span>{children}</span>;
    }
    function HTML() {
      return (
        <div>
          <Text>hello</Text>
          <Text>world</Text>
        </div>
      );
    }
    function RootModel() {
      return {
        html: <HTML />,
      };
    }

    // View
    function Message({response}) {
      return <section>{use(response).html}</section>;
    }
    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Message response={response} />
        </Suspense>
      );
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe(
      '<section><div><span>hello</span><span>world</span></div></section>',
    );
  });

  it('should not get confused by $', async () => {
    // Model
    function RootModel() {
      return {text: '$1'};
    }

    // View
    function Message({response}) {
      return <p>{use(response).text}</p>;
    }
    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Message response={response} />
        </Suspense>
      );
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>$1</p>');
  });

  it('should not get confused by @', async () => {
    // Model
    function RootModel() {
      return {text: '@div'};
    }

    // View
    function Message({response}) {
      return <p>{use(response).text}</p>;
    }
    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Message response={response} />
        </Suspense>
      );
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>@div</p>');
  });

  it('should be able to esm compat test module references', async () => {
    const ESMCompatModule = {
      __esModule: true,
      default: function ({greeting}) {
        return greeting + ' World';
      },
      hi: 'Hello',
    };

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    function interopWebpack(obj) {
      // Basically what Webpack's ESM interop feature testing does.
      if (typeof obj === 'object' && obj.__esModule) {
        return obj;
      }
      return Object.assign({default: obj}, obj);
    }

    const {default: Component, hi} = interopWebpack(
      clientExports(ESMCompatModule),
    );

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <Component greeting={hi} />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Hello World</p>');
  });

  it('should be able to render a named component export', async () => {
    const Module = {
      Component: function ({greeting}) {
        return greeting + ' World';
      },
    };

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const {Component} = clientExports(Module);

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <Component greeting={'Hello'} />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Hello World</p>');
  });

  it('should be able to render a module split named component export', async () => {
    const Module = {
      // This gets split into a separate module from the original one.
      split: function ({greeting}) {
        return greeting + ' World';
      },
    };

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const {split: Component} = clientExports(Module);

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <Component greeting={'Hello'} />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Hello World</p>');
  });

  it('should unwrap async module references', async () => {
    const AsyncModule = Promise.resolve(function AsyncModule({text}) {
      return 'Async: ' + text;
    });

    const AsyncModule2 = Promise.resolve({
      exportName: 'Module',
    });

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const AsyncModuleRef = await clientExports(AsyncModule);
    const AsyncModuleRef2 = await clientExports(AsyncModule2);

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Async: Module</p>');
  });

  it('should unwrap async module references using use', async () => {
    const AsyncModule = Promise.resolve('Async Text');

    function Print({response}) {
      return use(response);
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const AsyncModuleRef = clientExports(AsyncModule);

    function ServerComponent() {
      const text = FlightReact.use(AsyncModuleRef);
      return <p>{text}</p>;
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Async Text</p>');
  });

  it('should unwrap async ESM module references', async () => {
    const AsyncModule = Promise.resolve(function AsyncModule({text}) {
      return 'Async: ' + text;
    });

    const AsyncModule2 = Promise.resolve({
      exportName: 'Module',
    });

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const AsyncModuleRef = await clientExportsESM(AsyncModule);
    const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>Async: Module</p>');
  });

  it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => {
    const AsyncModule = Promise.resolve(function AsyncModule() {
      return 'This should not be rendered';
    });

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <ErrorBoundary
          fallback={error => (
            <p>
              {__DEV__ ? error.message + ' + ' : null}
              {error.digest}
            </p>
          )}>
          <Suspense fallback={<h1>Loading...</h1>}>
            <Print response={response} />
          </Suspense>
        </ErrorBoundary>
      );
    }

    const AsyncModuleRef = await clientExportsESM(AsyncModule, {
      forceClientModuleProxy: true,
    });

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <AsyncModuleRef />,
        webpackMap,
        {
          onError(error) {
            return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });

    const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;

    expect(container.innerHTML).toBe(
      __DEV__
        ? `<p>${errorMessage} + a dev digest</p>`
        : `<p>digest(${errorMessage})</p>`,
    );
  });

  it('should be able to import a name called "then"', async () => {
    const thenExports = {
      then: function then() {
        return 'and then';
      },
    };

    function Print({response}) {
      return <p>{use(response)}</p>;
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const ThenRef = clientExports(thenExports).then;

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<ThenRef />, webpackMap),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>and then</p>');
  });

  it('throws when accessing a member below the client exports', () => {
    const ClientModule = clientExports({
      Component: {deep: 'thing'},
    });
    function dotting() {
      return ClientModule.Component.deep;
    }
    expect(dotting).toThrowError(
      'Cannot access Component.deep on the server. ' +
        'You cannot dot into a client module from a server component. ' +
        'You can only pass the imported name through.',
    );
  });

  it('throws when await a client module prop of client exports', async () => {
    const ClientModule = clientExports({
      Component: {deep: 'thing'},
    });
    async function awaitExport() {
      const mod = await ClientModule;
      return await Promise.resolve(mod.Component);
    }
    await expect(awaitExport()).rejects.toThrowError(
      `Cannot await or return from a thenable. ` +
        `You cannot await a client module from a server component.`,
    );
  });

  it('throws when accessing a symbol prop from client exports', () => {
    const symbol = Symbol('test');
    const ClientModule = clientExports({
      Component: {deep: 'thing'},
    });
    function read() {
      return ClientModule[symbol];
    }
    expect(read).toThrowError(
      'Cannot read Symbol exports. ' +
        'Only named exports are supported on a client module imported on the server.',
    );
  });

  it('does not throw when toString:ing client exports', () => {
    const ClientModule = clientExports({
      Component: {deep: 'thing'},
    });
    expect(Object.prototype.toString.call(ClientModule)).toBe(
      '[object Object]',
    );
    expect(Object.prototype.toString.call(ClientModule.Component)).toBe(
      '[object Function]',
    );
  });

  it('does not throw when React inspects any deep props', () => {
    const ClientModule = clientExports({
      Component: function () {},
    });
    <ClientModule.Component key="this adds instrumentation" />;
  });

  it('throws when accessing a Context.Provider below the client exports', () => {
    const Context = React.createContext();
    const ClientModule = clientExports({
      Context,
    });
    function dotting() {
      return ClientModule.Context.Provider;
    }
    expect(dotting).toThrowError(
      `Cannot render a Client Context Provider on the Server. ` +
        `Instead, you can export a Client Component wrapper ` +
        `that itself renders a Client Context Provider.`,
    );
  });

  it('should progressively reveal server components', async () => {
    let reportedErrors = [];

    // Client Components

    function MyErrorBoundary({children}) {
      return (
        <ErrorBoundary
          fallback={e => (
            <p>
              {__DEV__ ? e.message + ' + ' : null}
              {e.digest}
            </p>
          )}>
          {children}
        </ErrorBoundary>
      );
    }

    // Model
    function Text({children}) {
      return children;
    }

    function makeDelayedText() {
      let _resolve, _reject;
      let promise = new Promise((resolve, reject) => {
        _resolve = () => {
          promise = null;
          resolve();
        };
        _reject = e => {
          promise = null;
          reject(e);
        };
      });
      async function DelayedText({children}) {
        await promise;
        return <Text>{children}</Text>;
      }
      return [DelayedText, _resolve, _reject];
    }

    const [Friends, resolveFriends] = makeDelayedText();
    const [Name, resolveName] = makeDelayedText();
    const [Posts, resolvePosts] = makeDelayedText();
    const [Photos, resolvePhotos] = makeDelayedText();
    const [Games, , rejectGames] = makeDelayedText();

    // View
    function ProfileDetails({avatar}) {
      return (
        <div>
          <Name>:name:</Name>
          {avatar}
        </div>
      );
    }
    function ProfileSidebar({friends}) {
      return (
        <div>
          <Photos>:photos:</Photos>
          {friends}
        </div>
      );
    }
    function ProfilePosts({posts}) {
      return <div>{posts}</div>;
    }
    function ProfileGames({games}) {
      return <div>{games}</div>;
    }

    const MyErrorBoundaryClient = clientExports(MyErrorBoundary);

    function ProfileContent() {
      return (
        <>
          <ProfileDetails avatar={<Text>:avatar:</Text>} />
          <Suspense fallback={<p>(loading sidebar)</p>}>
            <ProfileSidebar friends={<Friends>:friends:</Friends>} />
          </Suspense>
          <Suspense fallback={<p>(loading posts)</p>}>
            <ProfilePosts posts={<Posts>:posts:</Posts>} />
          </Suspense>
          <MyErrorBoundaryClient>
            <Suspense fallback={<p>(loading games)</p>}>
              <ProfileGames games={<Games>:games:</Games>} />
            </Suspense>
          </MyErrorBoundaryClient>
        </>
      );
    }

    const model = {
      rootContent: <ProfileContent />,
    };

    function ProfilePage({response}) {
      return use(response).rootContent;
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(model, webpackMap, {
        onError(x) {
          reportedErrors.push(x);
          return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
        },
      }),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <Suspense fallback={<p>(loading)</p>}>
          <ProfilePage response={response} />
        </Suspense>,
      );
    });
    expect(container.innerHTML).toBe('<p>(loading)</p>');

    // This isn't enough to show anything.
    await serverAct(async () => {
      await act(() => {
        resolveFriends();
      });
    });
    expect(container.innerHTML).toBe('<p>(loading)</p>');

    // We can now show the details. Sidebar and posts are still loading.
    await serverAct(async () => {
      await act(() => {
        resolveName();
      });
    });
    // Advance time enough to trigger a nested fallback.
    await act(() => {
      jest.advanceTimersByTime(500);
    });
    expect(container.innerHTML).toBe(
      '<div>:name::avatar:</div>' +
        '<p>(loading sidebar)</p>' +
        '<p>(loading posts)</p>' +
        '<p>(loading games)</p>',
    );

    expect(reportedErrors).toEqual([]);

    const theError = new Error('Game over');
    // Let's *fail* loading games.
    await serverAct(async () => {
      await act(async () => {
        await rejectGames(theError);
        await 'the inner async function';
      });
    });
    const expectedGamesValue = __DEV__
      ? '<p>Game over + a dev digest</p>'
      : '<p>digest("Game over")</p>';
    expect(container.innerHTML).toBe(
      '<div>:name::avatar:</div>' +
        '<p>(loading sidebar)</p>' +
        '<p>(loading posts)</p>' +
        expectedGamesValue,
    );

    expect(reportedErrors).toEqual([theError]);
    reportedErrors = [];

    // We can now show the sidebar.
    await serverAct(async () => {
      await act(async () => {
        await resolvePhotos();
        await 'the inner async function';
      });
    });
    expect(container.innerHTML).toBe(
      '<div>:name::avatar:</div>' +
        '<div>:photos::friends:</div>' +
        '<p>(loading posts)</p>' +
        expectedGamesValue,
    );

    // Show everything.
    await serverAct(async () => {
      await act(async () => {
        await resolvePosts();
        await 'the inner async function';
      });
    });
    expect(container.innerHTML).toBe(
      '<div>:name::avatar:</div>' +
        '<div>:photos::friends:</div>' +
        '<div>:posts:</div>' +
        expectedGamesValue,
    );

    expect(reportedErrors).toEqual([]);
  });

  it('should handle streaming async server components', async () => {
    const reportedErrors = [];

    const Row = async ({current, next}) => {
      const chunk = await next;

      if (chunk.done) {
        return chunk.value;
      }

      return (
        <Suspense fallback={chunk.value}>
          <Row current={chunk.value} next={chunk.next} />
        </Suspense>
      );
    };

    function createResolvablePromise() {
      let _resolve, _reject;

      const promise = new Promise((resolve, reject) => {
        _resolve = resolve;
        _reject = reject;
      });

      return {promise, resolve: _resolve, reject: _reject};
    }

    function createSuspendedChunk(initialValue) {
      const {promise, resolve, reject} = createResolvablePromise();

      return {
        row: (
          <Suspense fallback={initialValue}>
            <Row current={initialValue} next={promise} />
          </Suspense>
        ),
        resolve,
        reject,
      };
    }

    function makeDelayedText() {
      const {promise, resolve, reject} = createResolvablePromise();
      async function DelayedText() {
        const data = await promise;
        return <div>{data}</div>;
      }
      return [DelayedText, resolve, reject];
    }

    const [Posts, resolvePostsData] = makeDelayedText();
    const [Photos, resolvePhotosData] = makeDelayedText();
    const suspendedChunk = createSuspendedChunk(<p>loading</p>);
    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        suspendedChunk.row,
        webpackMap,
        {
          onError(error) {
            reportedErrors.push(error);
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function ClientRoot() {
      return use(response);
    }

    await act(() => {
      root.render(<ClientRoot />);
    });

    expect(container.innerHTML).toBe('<p>loading</p>');

    const donePromise = createResolvablePromise();

    const value = (
      <Suspense fallback={<p>loading posts and photos</p>}>
        <Posts />
        <Photos />
      </Suspense>
    );

    await serverAct(async () => {
      await act(async () => {
        suspendedChunk.resolve({value, done: false, next: donePromise.promise});
        donePromise.resolve({value, done: true});
      });
    });

    expect(container.innerHTML).toBe('<p>loading posts and photos</p>');

    await serverAct(async () => {
      await act(async () => {
        await resolvePostsData('posts');
        await resolvePhotosData('photos');
      });
    });

    expect(container.innerHTML).toBe('<div>posts</div><div>photos</div>');
    expect(reportedErrors).toEqual([]);
  });

  it('should preserve state of client components on refetch', async () => {
    // Client

    function Page({response}) {
      return use(response);
    }

    function Input() {
      return <input />;
    }

    const InputClient = clientExports(Input);

    // Server

    function App({color}) {
      // Verify both DOM and Client children.
      return (
        <div style={{color}}>
          <input />
          <InputClient />
        </div>
      );
    }

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    const stream1 = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <App color="red" />,
        webpackMap,
      ),
    );
    pipe(stream1.writable);
    const response1 = ReactServerDOMClient.createFromReadableStream(
      stream1.readable,
    );
    await act(() => {
      root.render(
        <Suspense fallback={<p>(loading)</p>}>
          <Page response={response1} />
        </Suspense>,
      );
    });
    expect(container.children.length).toBe(1);
    expect(container.children[0].tagName).toBe('DIV');
    expect(container.children[0].style.color).toBe('red');

    // Change the DOM state for both inputs.
    const inputA = container.children[0].children[0];
    expect(inputA.tagName).toBe('INPUT');
    inputA.value = 'hello';
    const inputB = container.children[0].children[1];
    expect(inputB.tagName).toBe('INPUT');
    inputB.value = 'goodbye';

    const stream2 = getTestStream();
    const {pipe: pipe2} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <App color="blue" />,
        webpackMap,
      ),
    );
    pipe2(stream2.writable);
    const response2 = ReactServerDOMClient.createFromReadableStream(
      stream2.readable,
    );
    await act(() => {
      root.render(
        <Suspense fallback={<p>(loading)</p>}>
          <Page response={response2} />
        </Suspense>,
      );
    });
    expect(container.children.length).toBe(1);
    expect(container.children[0].tagName).toBe('DIV');
    expect(container.children[0].style.color).toBe('blue');

    // Verify we didn't destroy the DOM for either input.
    expect(inputA === container.children[0].children[0]).toBe(true);
    expect(inputA.tagName).toBe('INPUT');
    expect(inputA.value).toBe('hello');
    expect(inputB === container.children[0].children[1]).toBe(true);
    expect(inputB.tagName).toBe('INPUT');
    expect(inputB.value).toBe('goodbye');
  });

  it('should be able to complete after aborting and throw the reason client-side', async () => {
    const reportedErrors = [];

    const {writable, readable} = getTestStream();
    const {pipe, abort} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <div>
          <InfiniteSuspend />
        </div>,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x);
            const message = typeof x === 'string' ? x : x.message;
            return __DEV__ ? 'a dev digest' : `digest("${message}")`;
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function App({res}) {
      return use(res);
    }

    await act(() => {
      root.render(
        <ErrorBoundary
          fallback={e => (
            <p>
              {__DEV__ ? e.message + ' + ' : null}
              {e.digest}
            </p>
          )}>
          <Suspense fallback={<p>(loading)</p>}>
            <App res={response} />
          </Suspense>
        </ErrorBoundary>,
      );
    });
    expect(container.innerHTML).toBe('<p>(loading)</p>');

    await act(() => {
      abort('for reasons');
    });
    if (__DEV__) {
      expect(container.innerHTML).toBe('<p>for reasons + a dev digest</p>');
    } else {
      expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
    }

    expect(reportedErrors).toEqual(['for reasons']);
  });

  it('should be able to recover from a direct reference erroring client-side', async () => {
    const reportedErrors = [];

    const ClientComponent = clientExports(function ({prop}) {
      return 'This should never render';
    });

    const ClientReference = clientModuleError(new Error('module init error'));

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <div>
          <ClientComponent prop={ClientReference} />
        </div>,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x);
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function App({res}) {
      return use(res);
    }

    await act(() => {
      root.render(
        <ErrorBoundary fallback={e => <p>{e.message}</p>}>
          <Suspense fallback={<p>(loading)</p>}>
            <App res={response} />
          </Suspense>
        </ErrorBoundary>,
      );
    });
    expect(container.innerHTML).toBe('<p>module init error</p>');

    expect(reportedErrors).toEqual([]);
  });

  it('should be able to recover from a direct reference erroring client-side async', async () => {
    const reportedErrors = [];

    const ClientComponent = clientExports(function ({prop}) {
      return 'This should never render';
    });

    let rejectPromise;
    const ClientReference = await clientExports(
      new Promise((resolve, reject) => {
        rejectPromise = reject;
      }),
    );

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <div>
          <ClientComponent prop={ClientReference} />
        </div>,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x);
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function App({res}) {
      return use(res);
    }

    await act(() => {
      root.render(
        <ErrorBoundary fallback={e => <p>{e.message}</p>}>
          <Suspense fallback={<p>(loading)</p>}>
            <App res={response} />
          </Suspense>
        </ErrorBoundary>,
      );
    });

    expect(container.innerHTML).toBe('<p>(loading)</p>');

    await act(() => {
      rejectPromise(new Error('async module init error'));
    });

    expect(container.innerHTML).toBe('<p>async module init error</p>');

    expect(reportedErrors).toEqual([]);
  });

  it('should be able to recover from a direct reference erroring server-side', async () => {
    const reportedErrors = [];

    const ClientComponent = clientExports(function ({prop}) {
      return 'This should never render';
    });

    // We simulate a bug in the Webpack bundler which causes an error on the server.
    for (const id in webpackMap) {
      Object.defineProperty(webpackMap, id, {
        get: () => {
          throw new Error('bug in the bundler');
        },
      });
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <div>
          <ClientComponent />
        </div>,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x.message);
            return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
          },
        },
      ),
    );
    pipe(writable);

    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    function App({res}) {
      return use(res);
    }

    await act(() => {
      root.render(
        <ErrorBoundary
          fallback={e => (
            <p>
              {__DEV__ ? e.message + ' + ' : null}
              {e.digest}
            </p>
          )}>
          <Suspense fallback={<p>(loading)</p>}>
            <App res={response} />
          </Suspense>
        </ErrorBoundary>,
      );
    });
    if (__DEV__) {
      expect(container.innerHTML).toBe(
        '<p>bug in the bundler + a dev digest</p>',
      );
    } else {
      expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
    }

    expect(reportedErrors).toEqual(['bug in the bundler']);
  });

  it('should pass a Promise through props and be able use() it on the client', async () => {
    async function getData() {
      return 'async hello';
    }

    function Component({data}) {
      const text = use(data);
      return <p>{text}</p>;
    }

    const ClientComponent = clientExports(Component);

    function ServerComponent() {
      const data = getData(); // no await here
      return <ClientComponent data={data} />;
    }

    function Print({response}) {
      return use(response);
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <Print response={response} />
        </Suspense>
      );
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe('<p>async hello</p>');
  });

  it('should throw on the client if a passed promise eventually rejects', async () => {
    const reportedErrors = [];
    const theError = new Error('Server throw');

    async function getData() {
      throw theError;
    }

    function Component({data}) {
      const text = use(data);
      return <p>{text}</p>;
    }

    const ClientComponent = clientExports(Component);

    function ServerComponent() {
      const data = getData(); // no await here
      return <ClientComponent data={data} />;
    }

    function Await({response}) {
      return use(response);
    }

    function App({response}) {
      return (
        <Suspense fallback={<h1>Loading...</h1>}>
          <ErrorBoundary
            fallback={e => (
              <p>
                {__DEV__ ? e.message + ' + ' : null}
                {e.digest}
              </p>
            )}>
            <Await response={response} />
          </ErrorBoundary>
        </Suspense>
      );
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x);
            return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
          },
        },
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App response={response} />);
    });
    expect(container.innerHTML).toBe(
      __DEV__
        ? '<p>Server throw + a dev digest</p>'
        : '<p>digest("Server throw")</p>',
    );
    expect(reportedErrors).toEqual([theError]);
  });

  it('should support float methods when rendering in Fiber', async () => {
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent() {
      FlightReactDOM.prefetchDNS('d before');
      FlightReactDOM.preconnect('c before');
      FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
      FlightReactDOM.preload('l before', {as: 'style'});
      FlightReactDOM.preloadModule('lm before');
      FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
      FlightReactDOM.preinit('i before', {as: 'script'});
      FlightReactDOM.preinitModule('m before');
      FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
      await 1;
      FlightReactDOM.prefetchDNS('d after');
      FlightReactDOM.preconnect('c after');
      FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
      FlightReactDOM.preload('l after', {as: 'style'});
      FlightReactDOM.preloadModule('lm after');
      FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
      FlightReactDOM.preinit('i after', {as: 'script'});
      FlightReactDOM.preinitModule('m after');
      FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
      return <ClientComponent />;
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );
    pipe(writable);

    let response = null;
    function getResponse() {
      if (response === null) {
        response = ReactServerDOMClient.createFromReadableStream(readable);
      }
      return response;
    }

    function App() {
      return getResponse();
    }

    // We pause to allow the float call after the await point to process before the
    // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
    // because the module graphs are not different and the HostDispatcher is shared.
    // In a real environment the Fiber and Flight code would each have their own independent
    // dispatcher.
    // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
    // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
    // but we need to make sure that actually makes sense
    await 1;

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });

    expect(getMeaningfulChildren(document)).toEqual(
      <html>
        <head>
          <link rel="dns-prefetch" href="d before" />
          <link rel="preconnect" href="c before" />
          <link rel="preconnect" href="c2 before" crossorigin="" />
          <link rel="preload" as="style" href="l before" />
          <link rel="modulepreload" href="lm before" />
          <link rel="modulepreload" href="lm2 before" crossorigin="" />
          <script async="" src="i before" />
          <script type="module" async="" src="m before" />
          <script type="module" async="" src="m2 before" crossorigin="" />
          <link rel="dns-prefetch" href="d after" />
          <link rel="preconnect" href="c after" />
          <link rel="preconnect" href="c2 after" crossorigin="" />
          <link rel="preload" as="style" href="l after" />
          <link rel="modulepreload" href="lm after" />
          <link rel="modulepreload" href="lm2 after" crossorigin="" />
          <script async="" src="i after" />
          <script type="module" async="" src="m after" />
          <script type="module" async="" src="m2 after" crossorigin="" />
        </head>
        <body />
      </html>,
    );
    expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
  });

  // @gate enablePostpone
  it('should allow postponing in Flight through a serialized promise', async () => {
    const Context = React.createContext();
    const ContextProvider = Context.Provider;

    function Foo() {
      const value = React.use(React.useContext(Context));
      return <span>{value}</span>;
    }

    const ClientModule = clientExports({
      ContextProvider,
      Foo,
    });

    async function getFoo() {
      React.unstable_postpone('foo');
    }

    function App() {
      return (
        <ClientModule.ContextProvider value={getFoo()}>
          <div>
            <Suspense fallback="loading...">
              <ClientModule.Foo />
            </Suspense>
          </div>
        </ClientModule.ContextProvider>
      );
    }

    const {writable, readable} = getTestStream();

    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
    );
    pipe(writable);

    let response = null;
    function getResponse() {
      if (response === null) {
        response = ReactServerDOMClient.createFromReadableStream(readable);
      }
      return response;
    }

    function Response() {
      return getResponse();
    }

    const errors = [];
    function onError(error, errorInfo) {
      errors.push(error, errorInfo);
    }
    const result = await serverAct(() =>
      ReactDOMStaticServer.prerenderToNodeStream(<Response />, {
        onError,
      }),
    );

    const prelude = await new Promise((resolve, reject) => {
      let content = '';
      result.prelude.on('data', chunk => {
        content += Buffer.from(chunk).toString('utf8');
      });
      result.prelude.on('error', error => {
        reject(error);
      });
      result.prelude.on('end', () => resolve(content));
    });

    expect(errors).toEqual([]);
    const doc = new JSDOM(prelude).window.document;
    expect(getMeaningfulChildren(doc)).toEqual(
      <html>
        <head />
        <body>
          <div>loading...</div>
        </body>
      </html>,
    );
  });

  it('should support float methods when rendering in Fizz', async () => {
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent() {
      FlightReactDOM.prefetchDNS('d before');
      FlightReactDOM.preconnect('c before');
      FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
      FlightReactDOM.preload('l before', {as: 'style'});
      FlightReactDOM.preloadModule('lm before');
      FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
      FlightReactDOM.preinit('i before', {as: 'script'});
      FlightReactDOM.preinitModule('m before');
      FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
      await 1;
      FlightReactDOM.prefetchDNS('d after');
      FlightReactDOM.preconnect('c after');
      FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
      FlightReactDOM.preload('l after', {as: 'style'});
      FlightReactDOM.preloadModule('lm after');
      FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
      FlightReactDOM.preinit('i after', {as: 'script'});
      FlightReactDOM.preinitModule('m after');
      FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
      return <ClientComponent />;
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();
    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    // In a real environment you would want to call the render during the Fizz render.
    // The reason we cannot do this in our test is because we don't actually have two separate
    // module graphs and we are contriving the sequencing to work in a way where
    // the right HostDispatcher is in scope during the Flight Server Float calls and the
    // Flight Client hint dispatches
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );
    pipe(flightWritable);

    let response = null;
    function getResponse() {
      if (response === null) {
        response =
          ReactServerDOMClient.createFromReadableStream(flightReadable);
      }
      return response;
    }

    function App() {
      return (
        <html>
          <body>{getResponse()}</body>
        </html>
      );
    }

    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
    });

    await readInto(document, fizzReadable);
    expect(getMeaningfulChildren(document)).toEqual(
      <html>
        <head>
          <link rel="dns-prefetch" href="d before" />
          <link rel="preconnect" href="c before" />
          <link rel="preconnect" href="c2 before" crossorigin="" />
          <link rel="dns-prefetch" href="d after" />
          <link rel="preconnect" href="c after" />
          <link rel="preconnect" href="c2 after" crossorigin="" />
          <script async="" src="i before" />
          <script type="module" async="" src="m before" />
          <script type="module" async="" src="m2 before" crossorigin="" />
          <script async="" src="i after" />
          <script type="module" async="" src="m after" />
          <script type="module" async="" src="m2 after" crossorigin="" />
          <link rel="preload" as="style" href="l before" />
          <link rel="modulepreload" href="lm before" />
          <link rel="modulepreload" href="lm2 before" crossorigin="" />
          <link rel="preload" as="style" href="l after" />
          <link rel="modulepreload" href="lm after" />
          <link rel="modulepreload" href="lm2 after" crossorigin="" />
        </head>
        <body>
          <p>hello world</p>
        </body>
      </html>,
    );
  });

  it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent1() {
      FlightReactDOM.preload('before1', {as: 'style'});
      await 1;
      FlightReactDOM.preload('after1', {as: 'style'});
      return <ClientComponent />;
    }

    async function ServerComponent2() {
      FlightReactDOM.preload('before2', {as: 'style'});
      await 1;
      FlightReactDOM.preload('after2', {as: 'style'});
      return <ClientComponent />;
    }

    const {writable: flightWritable1, readable: flightReadable1} =
      getTestStream();
    const {writable: flightWritable2, readable: flightReadable2} =
      getTestStream();

    ReactServerDOMServer.renderToPipeableStream(
      <ServerComponent1 />,
      webpackMap,
    ).pipe(flightWritable1);

    ReactServerDOMServer.renderToPipeableStream(
      <ServerComponent2 />,
      webpackMap,
    ).pipe(flightWritable2);

    const responses = new Map();
    function getResponse(stream) {
      let response = responses.get(stream);
      if (!response) {
        response = ReactServerDOMClient.createFromReadableStream(stream);
        responses.set(stream, response);
      }
      return response;
    }

    function App({stream}) {
      return (
        <html>
          <body>{getResponse(stream)}</body>
        </html>
      );
    }

    // pausing to let Flight runtime tick. This is a test only artifact of the fact that
    // we aren't operating separate module graphs for flight and fiber. In a real app
    // each would have their own dispatcher and there would be no cross dispatching.
    await serverAct(() => {});

    const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
    const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        <App stream={flightReadable1} />,
      ).pipe(fizzWritable1);
      ReactDOMFizzServer.renderToPipeableStream(
        <App stream={flightReadable2} />,
      ).pipe(fizzWritable2);
    });

    async function read(stream) {
      const decoder = new TextDecoder();
      const reader = stream.getReader();
      let buffer = '';
      while (true) {
        const {done, value} = await reader.read();
        if (done) {
          buffer += decoder.decode();
          break;
        }
        buffer += decoder.decode(value, {stream: true});
      }
      return buffer;
    }

    const [content1, content2] = await Promise.all([
      read(fizzReadable1),
      read(fizzReadable2),
    ]);

    expect(content1).toEqual(
      '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
        '<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
    );
    expect(content2).toEqual(
      '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
        '<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
    );
  });

  it('supports deduping hints by Float key', async () => {
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent() {
      FlightReactDOM.prefetchDNS('dns');
      FlightReactDOM.preconnect('preconnect');
      FlightReactDOM.preload('load', {as: 'style'});
      FlightReactDOM.preinit('init', {as: 'script'});
      // again but vary preconnect to demonstrate crossOrigin participates in the key
      FlightReactDOM.prefetchDNS('dns');
      FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
      FlightReactDOM.preload('load', {as: 'style'});
      FlightReactDOM.preinit('init', {as: 'script'});
      await 1;
      // after an async point
      FlightReactDOM.prefetchDNS('dns');
      FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
      FlightReactDOM.preload('load', {as: 'style'});
      FlightReactDOM.preinit('init', {as: 'script'});
      return <ClientComponent />;
    }

    const {writable, readable} = getTestStream();

    await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ).pipe(writable),
    );

    const hintRows = [];
    async function collectHints(stream) {
      const decoder = new TextDecoder();
      const reader = stream.getReader();
      let buffer = '';
      while (true) {
        const {done, value} = await reader.read();
        if (done) {
          buffer += decoder.decode();
          if (buffer.includes(':H')) {
            hintRows.push(buffer);
          }
          break;
        }
        buffer += decoder.decode(value, {stream: true});
        let line;
        while ((line = buffer.indexOf('\n')) > -1) {
          const row = buffer.slice(0, line);
          buffer = buffer.slice(line + 1);
          if (row.includes(':H')) {
            hintRows.push(row);
          }
        }
      }
    }

    await collectHints(readable);
    expect(hintRows.length).toEqual(6);
  });

  it('should be able to include a client reference in printed errors', async () => {
    const reportedErrors = [];

    const ClientComponent = clientExports(function ({prop}) {
      return 'This should never render';
    });

    const ClientReference = clientExports({});

    class InvalidValue {}

    const {writable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <div>
          <ClientComponent prop={ClientReference} invalid={InvalidValue} />
        </div>,
        webpackMap,
        {
          onError(x) {
            reportedErrors.push(x);
          },
        },
      ),
    );
    pipe(writable);

    expect(reportedErrors.length).toBe(1);
    if (__DEV__) {
      expect(reportedErrors[0].message).toEqual(
        'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
          'Or maybe you meant to call this function rather than return it.\n' +
          '  <... prop={client} invalid={function InvalidValue}>\n' +
          '                             ^^^^^^^^^^^^^^^^^^^^^^^',
      );
    } else {
      expect(reportedErrors[0].message).toEqual(
        'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
          'Or maybe you meant to call this function rather than return it.\n' +
          '  {prop: client, invalid: function InvalidValue}\n' +
          '                          ^^^^^^^^^^^^^^^^^^^^^',
      );
    }
  });

  it('should be able to render a client reference as return value', async () => {
    const ClientModule = clientExports({
      text: 'Hello World',
    });

    function ServerComponent() {
      return ClientModule.text;
    }

    const {writable, readable} = getTestStream();
    const {pipe} = await serverAct(() =>
      ReactServerDOMServer.renderToPipeableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );
    pipe(writable);
    const response = ReactServerDOMClient.createFromReadableStream(readable);

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    await act(() => {
      root.render(response);
    });
    expect(container.innerHTML).toBe('Hello World');
  });

  it('can abort synchronously during render', async () => {
    function Sibling() {
      return <p>sibling</p>;
    }

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>
            <ComponentThatAborts />
            <Sibling />
          </Suspense>
          <Suspense fallback={<p>loading 2...</p>}>
            <Sibling />
          </Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>
              <div>
                <Sibling />
              </div>
            </Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    function ComponentThatAborts() {
      abortRef.current();
      return <p>hello world</p>;
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>
          <p>loading 3...</p>
        </div>
      </div>,
    );
  });

  it('can abort during render in an async tick', async () => {
    async function Sibling() {
      return <p>sibling</p>;
    }

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>
            <ComponentThatAborts />
            <Sibling />
          </Suspense>
          <Suspense fallback={<p>loading 2...</p>}>
            <Sibling />
          </Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>
              <div>
                <Sibling />
              </div>
            </Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    async function ComponentThatAborts() {
      await 1;
      abortRef.current();
      return <p>hello world</p>;
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>
          <p>loading 3...</p>
        </div>
      </div>,
    );
  });

  it('can abort during render in a lazy initializer for a component', async () => {
    function Sibling() {
      return <p>sibling</p>;
    }

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>
            <LazyAbort />
          </Suspense>
          <Suspense fallback={<p>loading 2...</p>}>
            <Sibling />
          </Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>
              <div>
                <Sibling />
              </div>
            </Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    const LazyAbort = React.lazy(() => {
      abortRef.current();
      return {
        then(cb) {
          cb({default: 'div'});
        },
      };
    });

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>
          <p>loading 3...</p>
        </div>
      </div>,
    );
  });

  it('can abort during render in a lazy initializer for an element', async () => {
    function Sibling() {
      return <p>sibling</p>;
    }

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>{lazyAbort}</Suspense>
          <Suspense fallback={<p>loading 2...</p>}>
            <Sibling />
          </Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>
              <div>
                <Sibling />
              </div>
            </Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    const lazyAbort = React.lazy(() => {
      abortRef.current();
      return {
        then(cb) {
          cb({default: 'hello world'});
        },
      };
    });

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>
          <p>loading 3...</p>
        </div>
      </div>,
    );
  });

  it('can abort during a synchronous thenable resolution', async () => {
    function Sibling() {
      return <p>sibling</p>;
    }

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>{thenable}</Suspense>
          <Suspense fallback={<p>loading 2...</p>}>
            <Sibling />
          </Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>
              <div>
                <Sibling />
              </div>
            </Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    const thenable = {
      then(cb) {
        abortRef.current();
        cb(thenable.value);
      },
    };

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>
          <p>loading 3...</p>
        </div>
      </div>,
    );
  });

  it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading 1...</p>}>
            <ComponentThatAborts />
          </Suspense>
          <Suspense fallback={<p>loading 2...</p>}>{thenable1}</Suspense>
          <div>
            <Suspense fallback={<p>loading 3...</p>}>{thenable2}</Suspense>
          </div>
        </div>
      );
    }

    const abortRef = {current: null};
    const thenable1 = {
      then(cb) {
        cb('hello world');
      },
    };

    const thenable2 = {
      then(cb) {
        cb('hello world');
      },
      status: 'fulfilled',
      value: 'hello world',
    };

    function ComponentThatAborts() {
      abortRef.current();
      return thenable1;
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading 1...</p>
        <p>loading 2...</p>
        <div>hello world</div>
      </div>,
    );
  });

  it('can error synchronously after aborting without an unhandled rejection error', async () => {
    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading...</p>}>
            <ComponentThatAborts />
          </Suspense>
        </div>
      );
    }

    const abortRef = {current: null};

    async function ComponentThatAborts() {
      abortRef.current();
      throw new Error('boom');
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading...</p>
      </div>,
    );
  });

  it('can error synchronously after aborting in a synchronous Component', async () => {
    const rejectError = new Error('bam!');
    const rejectedPromise = Promise.reject(rejectError);
    rejectedPromise.catch(() => {});
    rejectedPromise.status = 'rejected';
    rejectedPromise.reason = rejectError;

    const resolvedValue = <p>hello world</p>;
    const resolvedPromise = Promise.resolve(resolvedValue);
    resolvedPromise.status = 'fulfilled';
    resolvedPromise.value = resolvedValue;

    function App() {
      return (
        <div>
          <Suspense fallback={<p>loading...</p>}>
            <ComponentThatAborts />
          </Suspense>
          <Suspense fallback={<p>loading too...</p>}>
            {rejectedPromise}
          </Suspense>
          <Suspense fallback={<p>loading three...</p>}>
            {resolvedPromise}
          </Suspense>
        </div>
      );
    }

    const abortRef = {current: null};

    // This test is specifically asserting that this works with Sync Server Component
    function ComponentThatAborts() {
      abortRef.current();
      throw new Error('boom');
    }

    const {writable: flightWritable, readable: flightReadable} =
      getTestStream();

    await serverAct(() => {
      const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
        <App />,
        webpackMap,
        {
          onError(e) {
            console.error(e);
          },
        },
      );
      abortRef.current = abort;
      pipe(flightWritable);
    });

    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'bam!',
    ]);

    const response =
      ReactServerDOMClient.createFromReadableStream(flightReadable);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });
    assertConsoleErrorDev([
      'The render was aborted by the server without a reason.',
      'bam!',
    ]);

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        <p>loading...</p>
        <p>loading too...</p>
        <p>hello world</p>
      </div>,
    );
  });

  // @gate experimental
  it('can prerender', async () => {
    let resolveGreeting;
    const greetingPromise = new Promise(resolve => {
      resolveGreeting = resolve;
    });

    function App() {
      return (
        <div>
          <Greeting />
        </div>
      );
    }

    async function Greeting() {
      await greetingPromise;
      return 'hello world';
    }

    const {pendingResult} = await serverAct(async () => {
      // destructure trick to avoid the act scope from awaiting the returned value
      return {
        pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
          <App />,
          webpackMap,
        ),
      };
    });

    resolveGreeting();
    const {prelude} = await pendingResult;

    const response = ReactServerDOMClient.createFromReadableStream(
      Readable.toWeb(prelude),
    );

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    const shellErrors = [];
    await serverAct(async () => {
      ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onShellError(error) {
            shellErrors.push(error.message);
          },
        },
      ).pipe(fizzWritable);
    });

    expect(shellErrors).toEqual([]);

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
  });

  // @gate enableHalt
  it('does not propagate abort reasons errors when aborting a prerender', async () => {
    let resolveGreeting;
    const greetingPromise = new Promise(resolve => {
      resolveGreeting = resolve;
    });

    function App() {
      return (
        <div>
          <Suspense fallback="loading...">
            <Greeting />
          </Suspense>
        </div>
      );
    }

    async function Greeting() {
      await greetingPromise;
      return 'hello world';
    }

    const controller = new AbortController();
    const errors = [];
    const {pendingResult} = await serverAct(async () => {
      // destructure trick to avoid the act scope from awaiting the returned value
      return {
        pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
          <App />,
          webpackMap,
          {
            signal: controller.signal,
            onError(err) {
              errors.push(err);
            },
          },
        ),
      };
    });

    controller.abort('boom');
    resolveGreeting();
    const {prelude} = await pendingResult;

    expect(errors).toEqual(['boom']);

    const preludeWeb = Readable.toWeb(prelude);
    const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }

    errors.length = 0;
    let abortFizz;
    await serverAct(async () => {
      const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onError(error) {
            errors.push(error);
          },
        },
      );
      pipe(fizzWritable);
      abortFizz = abort;
    });

    await serverAct(() => {
      abortFizz('bam');
    });

    if (__DEV__) {
      expect(errors).toEqual([new Error('Connection closed.')]);
    } else {
      // This is likely a bug. In Dev we get a connection closed error
      // because the debug info creates a chunk that has a pending status
      // and when the stream finishes we error if any chunks are still pending.
      // In production there is no debug info so the missing chunk is never instantiated
      // because nothing triggers model evaluation before the stream completes
      expect(errors).toEqual(['bam']);
    }

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
  });

  // @gate enableHalt
  it('will leave async iterables in an incomplete state when halting', async () => {
    let resolve;
    const wait = new Promise(r => (resolve = r));
    const errors = [];

    const multiShotIterable = {
      async *[Symbol.asyncIterator]() {
        yield {hello: 'A'};
        await wait;
        yield {hi: 'B'};
        return 'C';
      },
    };

    const controller = new AbortController();
    const {pendingResult} = await serverAct(() => {
      return {
        pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
          {
            multiShotIterable,
          },
          {},
          {
            onError(x) {
              errors.push(x);
            },
            signal: controller.signal,
          },
        ),
      };
    });

    controller.abort();
    await serverAct(() => resolve());

    const {prelude} = await pendingResult;

    const result = await ReactServerDOMClient.createFromReadableStream(
      Readable.toWeb(prelude),
    );

    const iterator = result.multiShotIterable[Symbol.asyncIterator]();

    expect(await iterator.next()).toEqual({
      value: {hello: 'A'},
      done: false,
    });

    const race = Promise.race([
      iterator.next(),
      new Promise(r => setTimeout(() => r('timeout'), 10)),
    ]);

    await 1;
    jest.advanceTimersByTime('100');
    expect(await race).toBe('timeout');
  });

  // @gate enableHalt
  it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
    const controller = new AbortController();
    function ComponentThatAborts() {
      controller.abort('boom');
      return null;
    }

    async function Greeting() {
      await 1;
      return 'hello world';
    }

    async function Farewell() {
      return 'goodbye world';
    }

    async function Wrapper() {
      return (
        <Suspense fallback="loading too...">
          <ComponentThatAborts />
        </Suspense>
      );
    }

    function App() {
      return (
        <div>
          <Suspense fallback="loading...">
            <Greeting />
          </Suspense>
          <Wrapper />
          <Suspense fallback="loading three...">
            <Farewell />
          </Suspense>
        </div>
      );
    }

    const errors = [];
    const {pendingResult} = await serverAct(() => {
      return {
        pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
          <App />,
          {},
          {
            onError(x) {
              errors.push(x);
            },
            signal: controller.signal,
          },
        ),
      };
    });

    const {prelude} = await pendingResult;

    expect(errors).toEqual(['boom']);

    const preludeWeb = Readable.toWeb(prelude);
    const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);

    const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

    function ClientApp() {
      return use(response);
    }
    errors.length = 0;
    let abortFizz;
    await serverAct(async () => {
      const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
        React.createElement(ClientApp),
        {
          onError(error, errorInfo) {
            errors.push(error);
          },
        },
      );
      pipe(fizzWritable);
      abortFizz = abort;
    });

    await serverAct(() => {
      abortFizz('boom');
    });

    // one error per boundary
    if (__DEV__) {
      const err = new Error('Connection closed.');
      expect(errors).toEqual([err, err, err]);
    } else {
      // This is likely a bug. In Dev we get a connection closed error
      // because the debug info creates a chunk that has a pending status
      // and when the stream finishes we error if any chunks are still pending.
      // In production there is no debug info so the missing chunk is never instantiated
      // because nothing triggers model evaluation before the stream completes
      expect(errors).toEqual(['boom', 'boom', 'boom']);
    }

    const container = document.createElement('div');
    await readInto(container, fizzReadable);
    expect(getMeaningfulChildren(container)).toEqual(
      <div>
        {'loading...'}
        {'loading too...'}
        {'loading three...'}
      </div>,
    );
  });
});