/**
 * 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';

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

const {
  patchMessageChannel,
} = require('../../../../scripts/jest/patchMessageChannel');

let clientExports;
let serverExports;
let webpackMap;
let webpackServerMap;
let act;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let Suspense;
let use;
let ReactServer;
let ReactServerDOM;
let Scheduler;
let ReactServerScheduler;
let reactServerAct;

describe('ReactFlightDOMBrowser', () => {
  beforeEach(() => {
    jest.resetModules();

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

    // Simulate the condition resolution

    jest.mock('react', () => require('react/react.react-server'));
    ReactServer = require('react');
    ReactServerDOM = require('react-dom');

    jest.mock('react-server-dom-webpack/server', () =>
      require('react-server-dom-webpack/server.browser'),
    );
    const WebpackMock = require('./utils/WebpackMock');
    clientExports = WebpackMock.clientExports;
    serverExports = WebpackMock.serverExports;
    webpackMap = WebpackMock.webpackMap;
    webpackServerMap = WebpackMock.webpackServerMap;
    ReactServerDOMServer = require('react-server-dom-webpack/server');
    if (__EXPERIMENTAL__) {
      jest.mock('react-server-dom-webpack/static', () =>
        require('react-server-dom-webpack/static.browser'),
      );
      ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
    }

    __unmockReact();
    jest.resetModules();

    Scheduler = require('scheduler');
    patchMessageChannel(Scheduler);

    act = require('internal-test-utils').act;
    React = require('react');
    ReactDOM = require('react-dom');
    ReactDOMClient = require('react-dom/client');
    ReactDOMFizzServer = require('react-dom/server.browser');
    ReactServerDOMClient = require('react-server-dom-webpack/client');
    Suspense = React.Suspense;
    use = React.use;
  });

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

  function makeDelayedText(Model) {
    let error, _resolve, _reject;
    let promise = new Promise((resolve, reject) => {
      _resolve = () => {
        promise = null;
        resolve();
      };
      _reject = e => {
        error = e;
        promise = null;
        reject(e);
      };
    });
    function DelayedText({children}, data) {
      if (promise) {
        throw promise;
      }
      if (error) {
        throw error;
      }
      return <Model>{children}</Model>;
    }
    return [DelayedText, _resolve, _reject];
  }

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

  function requireServerRef(ref) {
    let name = '';
    let resolvedModuleData = webpackServerMap[ref];
    if (resolvedModuleData) {
      // The potentially aliased name.
      name = resolvedModuleData.name;
    } else {
      // We didn't find this specific export name but we might have the * export
      // which contains this name as well.
      // TODO: It's unfortunate that we now have to parse this string. We should
      // probably go back to encoding path and name separately on the client reference.
      const idx = ref.lastIndexOf('#');
      if (idx !== -1) {
        name = ref.slice(idx + 1);
        resolvedModuleData = webpackServerMap[ref.slice(0, idx)];
      }
      if (!resolvedModuleData) {
        throw new Error(
          'Could not find the module "' +
            ref +
            '" in the React Client Manifest. ' +
            'This is probably a bug in the React Server Components bundler.',
        );
      }
    }
    const mod = __webpack_require__(resolvedModuleData.id);
    if (name === '*') {
      return mod;
    }
    return mod[name];
  }

  async function callServer(actionId, body) {
    const fn = requireServerRef(actionId);
    const args = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
    return fn.apply(null, args);
  }

  it('should resolve HTML using W3C 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 stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<App />),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const model = await response;
    expect(model).toEqual({
      html: (
        <div>
          <span>hello</span>
          <span>world</span>
        </div>
      ),
    });
  });

  it('should resolve client components (with async chunks) when referenced in props', async () => {
    let resolveClientComponentChunk;

    const ClientOuter = clientExports(function ClientOuter({
      Component,
      children,
    }) {
      return <Component>{children}</Component>;
    });

    const ClientInner = clientExports(
      function ClientInner({children}) {
        return <span>{children}</span>;
      },
      '42',
      '/test.js',
      new Promise(resolve => (resolveClientComponentChunk = resolve)),
    );

    function Server() {
      return <ClientOuter Component={ClientInner}>Hello, World!</ClientOuter>;
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<span>Hello, World!</span>');
  });

  it('should resolve deduped objects within the same model root when it is blocked', async () => {
    let resolveClientComponentChunk;

    const ClientOuter = clientExports(function ClientOuter({Component, value}) {
      return <Component value={value} />;
    });

    const ClientInner = clientExports(
      function ClientInner({value}) {
        return <pre>{JSON.stringify(value)}</pre>;
      },
      '42',
      '/test.js',
      new Promise(resolve => (resolveClientComponentChunk = resolve)),
    );

    function Server({value}) {
      return <ClientOuter Component={ClientInner} value={value} />;
    }

    const shared = [1, 2, 3];
    const value = [shared, shared];

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <Server value={value} />,
        webpackMap,
      ),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
  });

  it('should resolve deduped objects within the same model root when it is blocked and there is a listener attached to the root', async () => {
    let resolveClientComponentChunk;

    const ClientOuter = clientExports(function ClientOuter({Component, value}) {
      return <Component value={value} />;
    });

    const ClientInner = clientExports(
      function ClientInner({value}) {
        return <pre>{JSON.stringify(value)}</pre>;
      },
      '42',
      '/test.js',
      new Promise(resolve => (resolveClientComponentChunk = resolve)),
    );

    function Server({value}) {
      return <ClientOuter Component={ClientInner} value={value} />;
    }

    const shared = [1, 2, 3];
    const value = [shared, shared];

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <Server value={value} />,
        webpackMap,
      ),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    // make sure we have a listener so that `resolveModelChunk` initializes the chunk eagerly
    response.then(() => {});

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

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
  });

  it('should resolve deduped objects that are themselves blocked', async () => {
    let resolveClientComponentChunk;

    const Client = clientExports(
      [4, 5],
      '42',
      '/test.js',
      new Promise(resolve => (resolveClientComponentChunk = resolve)),
    );

    const shared = [1, 2, 3, Client];

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <div>
          <Suspense fallback="Loading">
            <span>
              {shared /* this will serialize first and block nearest element */}
            </span>
          </Suspense>
          {shared /* this will be referenced inside the blocked element */}
        </div>,
        webpackMap,
      ),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<div><span>12345</span>12345</div>');
  });

  it('should resolve deduped objects in nested children of blocked models', async () => {
    let resolveOuterClientComponentChunk;
    let resolveInnerClientComponentChunk;

    const ClientOuter = clientExports(
      function ClientOuter({children, value}) {
        return children;
      },
      '1',
      '/outer.js',
      new Promise(resolve => (resolveOuterClientComponentChunk = resolve)),
    );

    function PassthroughServerComponent({children}) {
      return children;
    }

    const ClientInner = clientExports(
      function ClientInner({children}) {
        return JSON.stringify(children);
      },
      '2',
      '/inner.js',
      new Promise(resolve => (resolveInnerClientComponentChunk = resolve)),
    );

    const value = {};

    function Server() {
      return (
        <ClientOuter value={value}>
          <PassthroughServerComponent>
            <ClientInner>{value}</ClientInner>
          </PassthroughServerComponent>
        </ClientOuter>
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveInnerClientComponentChunk();
      resolveOuterClientComponentChunk();
    });

    expect(container.innerHTML).toBe('{}');
  });

  it('should resolve deduped objects in blocked models referencing other blocked models with blocked references', async () => {
    let resolveFooClientComponentChunk;
    let resolveBarClientComponentChunk;

    function PassthroughServerComponent({children}) {
      return children;
    }

    const FooClient = clientExports(
      function FooClient({children}) {
        return JSON.stringify(children);
      },
      '1',
      '/foo.js',
      new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
    );

    const BarClient = clientExports(
      function BarClient() {
        return 'not used';
      },
      '2',
      '/bar.js',
      new Promise(resolve => (resolveBarClientComponentChunk = resolve)),
    );

    const shared = {foo: 1};

    function Server() {
      return (
        <>
          <PassthroughServerComponent>
            <FooClient key="first" bar={BarClient}>
              {shared}
            </FooClient>
          </PassthroughServerComponent>
          <FooClient key="second" bar={BarClient}>
            {shared}
          </FooClient>
        </>
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveFooClientComponentChunk();
      resolveBarClientComponentChunk();
    });

    expect(container.innerHTML).toBe('{"foo":1}{"foo":1}');
  });

  it('should handle deduped props of re-used elements in fragments (same-chunk reference)', async () => {
    let resolveFooClientComponentChunk;

    const FooClient = clientExports(
      function Foo({children, item}) {
        return children;
      },
      '1',
      '/foo.js',
      new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
    );

    const shared = <div />;

    function Server() {
      return (
        <FooClient track={shared}>
          <>{shared}</>
        </FooClient>
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveFooClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<div></div>');
  });

  it('should handle deduped props of re-used elements in server components (cross-chunk reference)', async () => {
    let resolveFooClientComponentChunk;

    function PassthroughServerComponent({children}) {
      return children;
    }

    const FooClient = clientExports(
      function Foo({children, item}) {
        return children;
      },
      '1',
      '/foo.js',
      new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
    );

    const shared = <div />;

    function Server() {
      return (
        <FooClient track={shared}>
          <PassthroughServerComponent>{shared}</PassthroughServerComponent>
        </FooClient>
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    const response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    expect(container.innerHTML).toBe('');

    await act(() => {
      resolveFooClientComponentChunk();
    });

    expect(container.innerHTML).toBe('<div></div>');
  });

  it('should handle references to deduped owner objects', async () => {
    // This is replicating React components as generated by @svgr/webpack:
    let path1a: React.ReactNode;
    let path1b: React.ReactNode;
    let path2: React.ReactNode;

    function Svg1() {
      return ReactServer.createElement(
        'svg',
        {id: '1'},
        path1a || (path1a = ReactServer.createElement('path', {})),
        path1b || (path1b = ReactServer.createElement('path', {})),
      );
    }

    function Svg2() {
      return ReactServer.createElement(
        'svg',
        {id: '2'},
        path2 || (path2 = ReactServer.createElement('path', {})),
      );
    }

    function Server() {
      return ReactServer.createElement(
        ReactServer.Fragment,
        {},
        ReactServer.createElement(Svg1),
        ReactServer.createElement(Svg2),
      );
    }

    let stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

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

    let response = ReactServerDOMClient.createFromReadableStream(stream);
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

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

    const expectedHtml =
      '<svg id="1"><path></path><path></path></svg><svg id="2"><path></path></svg>';

    expect(container.innerHTML).toBe(expectedHtml);

    // Render a second time:

    // Assigning the path elements to variables in module scope (here simulated
    // with the test's function scope), and rendering a second time, prevents
    // the owner of the path elements (i.e. Svg1/Svg2) to be deduped. The owner
    // of the path in Svg1 is fully inlined. The owner of the owner of the path
    // in Svg2 is Server, which is deduped and replaced with a reference to the
    // owner of the owner of the path in Svg1. This nested owner is actually
    // Server from the previous render pass, which is kinda broken and libraries
    // probably shouldn't generate code like this. This reference can only be
    // resolved properly if owners are specifically handled when resolving
    // outlined models.

    stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
    );

    response = ReactServerDOMClient.createFromReadableStream(stream);

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

    expect(container.innerHTML).toBe(expectedHtml);

    if (__DEV__) {
      const resolvedPath1b = response.value[0].props.children[1];

      expect(resolvedPath1b._owner).toEqual(
        expect.objectContaining({
          name: 'Svg1',
          env: 'Server',
          key: null,
          owner: expect.objectContaining({
            name: 'Server',
            env: 'Server',
            key: null,
            owner: null,
          }),
        }),
      );

      const resolvedPath2 = response.value[1].props.children;

      expect(resolvedPath2._owner).toEqual(
        expect.objectContaining({
          name: 'Svg2',
          env: 'Server',
          key: null,
          owner: expect.objectContaining({
            name: 'Server',
            env: 'Server',
            key: null,
            owner: null,
          }),
        }),
      );
    }
  });

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

    // Client Components

    class ErrorBoundary 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;
      }
    }

    let errorBoundaryFn;
    if (__DEV__) {
      errorBoundaryFn = e => (
        <p>
          {e.message} + {e.digest}
        </p>
      );
    } else {
      errorBoundaryFn = e => {
        expect(e.message).toBe(
          'An error occurred in the Server Components render. The specific message is omitted in production' +
            ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
            ' may provide additional details about the nature of the error.',
        );
        return <p>{e.digest}</p>;
      };
    }

    function MyErrorBoundary({children}) {
      return (
        <ErrorBoundary fallback={errorBoundaryFn}>{children}</ErrorBoundary>
      );
    }

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

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

    // 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 stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
        onError(x) {
          reportedErrors.push(x);
          return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
        },
      }),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

    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.
    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(() => {
        rejectGames(theError);
      });
    });

    const gamesExpectedValue = __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>' +
        gamesExpectedValue,
    );

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

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

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

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

  it('should close the stream upon completion when rendering to W3C streams', async () => {
    // Model
    function Text({children}) {
      return children;
    }

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

    // 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 ProfileContent() {
      return (
        <Suspense fallback="(loading everything)">
          <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>
        </Suspense>
      );
    }

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

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(model, webpackMap),
    );

    const reader = stream.getReader();
    const decoder = new TextDecoder();

    let flightResponse = '';
    let isDone = false;

    reader.read().then(function progress({done, value}) {
      if (done) {
        isDone = true;
        return;
      }

      flightResponse += decoder.decode(value);

      return reader.read().then(progress);
    });

    // Advance time enough to trigger a nested fallback.
    jest.advanceTimersByTime(500);

    await serverAct(() => {});

    expect(flightResponse).toContain('(loading everything)');
    expect(flightResponse).toContain('(loading sidebar)');
    expect(flightResponse).toContain('(loading posts)');
    if (!__DEV__) {
      expect(flightResponse).not.toContain(':friends:');
      expect(flightResponse).not.toContain(':name:');
    }

    await serverAct(() => {
      resolveFriends();
    });

    expect(flightResponse).toContain(':friends:');

    await serverAct(() => {
      resolveName();
    });

    expect(flightResponse).toContain(':name:');

    await serverAct(() => {
      resolvePhotos();
    });

    expect(flightResponse).toContain(':photos:');

    await serverAct(() => {
      resolvePosts();
    });

    expect(flightResponse).toContain(':posts:');

    // Final pending chunk is written; stream should be closed.
    expect(isDone).toBeTruthy();
  });

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

    let errorBoundaryFn;
    if (__DEV__) {
      errorBoundaryFn = e => (
        <p>
          {e.message} + {e.digest}
        </p>
      );
    } else {
      errorBoundaryFn = e => {
        expect(e.message).toBe(
          'An error occurred in the Server Components render. The specific message is omitted in production' +
            ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
            ' may provide additional details about the nature of the error.',
        );
        return <p>{e.digest}</p>;
      };
    }

    class ErrorBoundary 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;
      }
    }

    const controller = new AbortController();
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <div>
          <InfiniteSuspend />
        </div>,
        webpackMap,
        {
          signal: controller.signal,
          onError(x) {
            const message = typeof x === 'string' ? x : x.message;
            reportedErrors.push(x);
            return __DEV__ ? 'a dev digest' : `digest("${message}")`;
          },
        },
      ),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

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

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

    await act(() => {
      controller.abort('for reasons');
    });
    const expectedValue = __DEV__
      ? '<p>for reasons + a dev digest</p>'
      : '<p>digest("for reasons")</p>';
    expect(container.innerHTML).toBe(expectedValue);

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

  it('should warn in DEV a child is missing keys', async () => {
    function ParentClient({children}) {
      return children;
    }
    const Parent = clientExports(ParentClient);
    const ParentModule = clientExports({Parent: ParentClient});

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

    await expect(async () => {
      const stream = await serverAct(() =>
        ReactServerDOMServer.renderToReadableStream(
          <>
            <Parent>{Array(6).fill(<div>no key</div>)}</Parent>
            <ParentModule.Parent>
              {Array(6).fill(<div>no key</div>)}
            </ParentModule.Parent>
          </>,
          webpackMap,
        ),
      );
      const result =
        await ReactServerDOMClient.createFromReadableStream(stream);

      await act(() => {
        root.render(result);
      });
    }).toErrorDev('Each child in a list should have a unique "key" prop.');
  });

  it('basic use(promise)', async () => {
    function Server() {
      return (
        ReactServer.use(Promise.resolve('A')) +
        ReactServer.use(Promise.resolve('B')) +
        ReactServer.use(Promise.resolve('C'))
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <Suspense fallback="Loading...">
          <Client />
        </Suspense>,
      );
    });
    expect(container.innerHTML).toBe('ABC');
  });

  it('use(promise) in multiple components', async () => {
    function Child({prefix}) {
      return (
        prefix +
        ReactServer.use(Promise.resolve('C')) +
        ReactServer.use(Promise.resolve('D'))
      );
    }

    function Parent() {
      return (
        <Child
          prefix={
            ReactServer.use(Promise.resolve('A')) +
            ReactServer.use(Promise.resolve('B'))
          }
        />
      );
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Parent />),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <Suspense fallback="Loading...">
          <Client />
        </Suspense>,
      );
    });
    expect(container.innerHTML).toBe('ABCD');
  });

  it('using a rejected promise will throw', async () => {
    const promiseA = Promise.resolve('A');
    const promiseB = Promise.reject(new Error('Oops!'));
    const promiseC = Promise.resolve('C');

    // Jest/Node will raise an unhandled rejected error unless we await this. It
    // works fine in the browser, though.
    await expect(promiseB).rejects.toThrow('Oops!');

    function Server() {
      return (
        ReactServer.use(promiseA) +
        ReactServer.use(promiseB) +
        ReactServer.use(promiseC)
      );
    }

    const reportedErrors = [];
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap, {
        onError(x) {
          reportedErrors.push(x);
          return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
        },
      }),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

    class ErrorBoundary extends React.Component {
      state = {error: null};
      static getDerivedStateFromError(error) {
        return {error};
      }
      render() {
        if (this.state.error) {
          return __DEV__
            ? this.state.error.message + ' + ' + this.state.error.digest
            : this.state.error.digest;
        }
        return this.props.children;
      }
    }

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <ErrorBoundary>
          <Client />
        </ErrorBoundary>,
      );
    });
    expect(container.innerHTML).toBe(
      __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
    );
    expect(reportedErrors.length).toBe(1);
    expect(reportedErrors[0].message).toBe('Oops!');
  });

  it("use a promise that's already been instrumented and resolved", async () => {
    const thenable = {
      status: 'fulfilled',
      value: 'Hi',
      then() {},
    };

    // This will never suspend because the thenable already resolved
    function Server() {
      return ReactServer.use(thenable);
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

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

  it('unwraps thenable that fulfills synchronously without suspending', async () => {
    function Server() {
      const thenable = {
        then(resolve) {
          // This thenable immediately resolves, synchronously, without waiting
          // a microtask.
          resolve('Hi');
        },
      };
      try {
        return ReactServer.use(thenable);
      } catch {
        throw new Error(
          '`use` should not suspend because the thenable resolved synchronously.',
        );
      }
    }

    // Because the thenable resolves synchronously, we should be able to finish
    // rendering synchronously, with no fallback.
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(<Server />),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

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

  it('can pass a higher order function by reference from server to client', async () => {
    let actionProxy;

    function Client({action}) {
      actionProxy = action;
      return 'Click Me';
    }

    function greet(transform, text) {
      return 'Hello ' + transform(text);
    }

    function upper(text) {
      return text.toUpperCase();
    }

    const ServerModuleA = serverExports({
      greet,
    });
    const ServerModuleB = serverExports({
      upper,
    });
    const ClientRef = clientExports(Client);

    const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef action={boundFn} />,
        webpackMap,
      ),
    );

    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(ref, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        return callServer(ref, body);
      },
    });

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });
    expect(container.innerHTML).toBe('Click Me');
    expect(typeof actionProxy).toBe('function');
    expect(actionProxy).not.toBe(boundFn);

    const result = await actionProxy('hi');
    expect(result).toBe('Hello HI');
  });

  it('can call a module split server function', async () => {
    let actionProxy;

    function Client({action}) {
      actionProxy = action;
      return 'Click Me';
    }

    function greet(text) {
      return 'Hello ' + text;
    }

    const ServerModule = serverExports({
      // This gets split into another module
      split: greet,
    });
    const ClientRef = clientExports(Client);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef action={ServerModule.split} />,
        webpackMap,
      ),
    );

    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(ref, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        return callServer(ref, body);
      },
    });

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });
    expect(container.innerHTML).toBe('Click Me');
    expect(typeof actionProxy).toBe('function');

    const result = await actionProxy('Split');
    expect(result).toBe('Hello Split');
  });

  it('can pass a server function by importing from client back to server', async () => {
    function greet(transform, text) {
      return 'Hello ' + transform(text);
    }

    function upper(text) {
      return text.toUpperCase();
    }

    const ServerModuleA = serverExports({
      greet,
    });
    const ServerModuleB = serverExports({
      upper,
    });

    let actionProxy;

    // This is a Proxy representing ServerModuleB in the Client bundle.
    const ServerModuleBImportedOnClient = {
      upper: ReactServerDOMClient.createServerReference(
        ServerModuleB.upper.$$id,
        async function (ref, args) {
          const body = await ReactServerDOMClient.encodeReply(args);
          return callServer(ref, body);
        },
        undefined,
        undefined,
        'upper',
      ),
    };

    expect(ServerModuleBImportedOnClient.upper.name).toBe(
      __DEV__ ? 'upper' : 'action',
    );
    if (__DEV__) {
      expect(ServerModuleBImportedOnClient.upper.toString()).toBe(
        '(...args) => server(...args)',
      );
    }

    function Client({action}) {
      // Client side pass a Server Reference into an action.
      actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
      return 'Click Me';
    }

    const ClientRef = clientExports(Client);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef action={ServerModuleA.greet} />,
        webpackMap,
      ),
    );

    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(ref, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        return callServer(ref, body);
      },
    });

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

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

    const result = await actionProxy('hi');
    expect(result).toBe('Hello HI');
  });

  it('can bind arguments to a server reference', async () => {
    let actionProxy;

    function Client({action}) {
      actionProxy = action;
      return 'Click Me';
    }

    const greet = serverExports(function greet(a, b, c) {
      return a + ' ' + b + c;
    });
    const ClientRef = clientExports(Client);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef action={greet.bind(null, 'Hello').bind(null, 'World')} />,
        webpackMap,
      ),
    );

    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(actionId, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        return callServer(actionId, body);
      },
    });

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });
    expect(container.innerHTML).toBe('Click Me');
    expect(typeof actionProxy).toBe('function');
    expect(actionProxy).not.toBe(greet);

    const result = await actionProxy('!');
    expect(result).toBe('Hello World!');
  });

  it('propagates server reference errors to the client', async () => {
    let actionProxy;

    function Client({action}) {
      actionProxy = action;
      return 'Click Me';
    }

    async function send(text) {
      throw new Error(`Error for ${text}`);
    }

    const ServerModule = serverExports({send});
    const ClientRef = clientExports(Client);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef action={ServerModule.send} />,
        webpackMap,
      ),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(actionId, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        const result = callServer(actionId, body);
        // Flight doesn't attach error handlers early enough. we suppress the warning
        // by putting a dummy catch on the result here
        result.catch(() => {});
        return ReactServerDOMClient.createFromReadableStream(
          ReactServerDOMServer.renderToReadableStream(result, null, {
            onError: error => 'test-error-digest',
          }),
        );
      },
    });

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

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

    let thrownError;

    try {
      await serverAct(() => actionProxy('test'));
    } catch (error) {
      thrownError = error;
    }

    if (__DEV__) {
      expect(thrownError).toEqual(new Error('Error for test'));
    } else {
      expect(thrownError).toEqual(
        new Error(
          'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
        ),
      );

      expect(thrownError.digest).toBe('test-error-digest');
    }
  });

  it('can use the same function twice as a server action', async () => {
    let actionProxy1;
    let actionProxy2;

    function Client({action1, action2}) {
      actionProxy1 = action1;
      actionProxy2 = action2;
      return 'Click Me';
    }

    function greet(text) {
      return 'Hello ' + text;
    }

    const ServerModule = serverExports({
      greet,
      greet2: greet,
    });
    const ClientRef = clientExports(Client);

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ClientRef
          action1={ServerModule.greet}
          action2={ServerModule.greet2}
        />,
        webpackMap,
      ),
    );

    const response = ReactServerDOMClient.createFromReadableStream(stream, {
      async callServer(ref, args) {
        const body = await ReactServerDOMClient.encodeReply(args);
        return callServer(ref, body);
      },
    });

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });
    expect(container.innerHTML).toBe('Click Me');
    expect(typeof actionProxy1).toBe('function');
    expect(actionProxy1).not.toBe(greet);

    // TODO: Ideally flight would be encoding this the same.
    expect(actionProxy1).not.toBe(actionProxy2);

    const result = await actionProxy1('world');
    expect(result).toBe('Hello world');
  });

  it('can pass an async server exports that resolves later to an outline object like a Map', async () => {
    let resolve;
    const chunkPromise = new Promise(r => (resolve = r));

    function action() {}
    const serverModule = serverExports(
      {
        action: action,
      },
      chunkPromise,
    );

    // Send the action to the client
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        {action: serverModule.action},
        webpackMap,
      ),
    );
    const response =
      await ReactServerDOMClient.createFromReadableStream(stream);

    // Pass the action back to the server inside a Map

    const map = new Map();
    map.set('action', response.action);

    const body = await ReactServerDOMClient.encodeReply(map);
    const resultPromise = ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );

    // We couldn't yet resolve the server reference because we haven't loaded
    // its chunk yet in the new server instance. We now resolve it which loads
    // it asynchronously.
    await resolve();

    const result = await resultPromise;
    expect(result instanceof Map).toBe(true);
    expect(result.get('action')).toBe(action);
  });

  it('supports Float hints before the first await in server components in Fiber', async () => {
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent() {
      ReactServerDOM.preload('before', {as: 'style'});
      await 1;
      ReactServerDOM.preload('after', {as: 'style'});
      return <ClientComponent />;
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );

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

    function App() {
      return getResponse();
    }

    // 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 1;

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<App />);
    });
    expect(document.head.innerHTML).toBe(
      gate(f => f.www)
        ? // The www entrypoints for ReactDOM and ReactDOMClient are unified so even
          // when you pull in just the top level the dispatcher for the Document is
          // loaded alongside it. In a normal environment there would be nothing to dispatch to
          // in a server environment so the preload calls would still only be dispatched to fizz
          // or the browser but not both. However in this contrived test environment the preloads
          // are being dispatched simultaneously causing an extraneous preload to show up. This test currently
          // asserts this be demonstrating that the preload call after the await point
          // is written to the document before the call before it. We still demonstrate that
          // flight handled the sync call because if the fiber implementation did it would appear
          // before the after call. In the future we will change this assertion once the fiber
          // implementation no long automatically gets pulled in
          '<link rel="preload" href="after" as="style"><link rel="preload" href="before" as="style">'
        : // For other release channels the client and isomorphic entrypoints are separate and thus we only
          // observe the expected preload from before the first await
          '<link rel="preload" href="before" as="style">',
    );
    expect(container.innerHTML).toBe('<p>hello world</p>');
  });

  it('Does not support Float hints in server components anywhere in Fizz', async () => {
    // In environments that do not support AsyncLocalStorage the Flight client has no ability
    // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
    // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
    // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
    function Component() {
      return <p>hello world</p>;
    }

    const ClientComponent = clientExports(Component);

    async function ServerComponent() {
      ReactDOM.preload('before', {as: 'style'});
      await 1;
      ReactDOM.preload('after', {as: 'style'});
      return <ClientComponent />;
    }

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <ServerComponent />,
        webpackMap,
      ),
    );

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

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

    let fizzPromise;
    await act(async () => {
      fizzPromise = ReactDOMFizzServer.renderToReadableStream(<App />);
    });
    const fizzStream = await fizzPromise;

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

    expect(content).toEqual(
      '<!DOCTYPE html><html><head>' +
        '</head><body><p>hello world</p></body></html>',
    );
  });

  // @gate enablePostpone
  it('supports postpone in Server Components', async () => {
    function Server() {
      React.unstable_postpone('testing postpone');
      return 'Not shown';
    }

    let postponed = null;

    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <Suspense fallback="Loading...">
          <Server />
        </Suspense>,
        null,
        {
          onPostpone(reason) {
            postponed = reason;
          },
        },
      ),
    );
    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(async () => {
      root.render(
        <div>
          Shell: <Client />
        </div>,
      );
    });
    // We should have reserved the shell already. Which means that the Server
    // Component should've been a lazy component.
    expect(container.innerHTML).toContain('Shell:');
    expect(container.innerHTML).toContain('Loading...');
    expect(container.innerHTML).not.toContain('Not shown');

    expect(postponed).toBe('testing postpone');
  });

  it('should not continue rendering after the reader cancels', async () => {
    let hasLoaded = false;
    let resolve;
    let rendered = false;
    const promise = new Promise(r => (resolve = r));
    function Wait() {
      if (!hasLoaded) {
        throw promise;
      }
      rendered = true;
      return 'Done';
    }
    const errors = [];
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <div>
          <Suspense fallback={<div>Loading</div>}>
            <Wait />
          </Suspense>
        </div>,
        null,
        {
          onError(x) {
            errors.push(x.message);
          },
        },
      ),
    );

    expect(rendered).toBe(false);

    const reader = stream.getReader();
    await reader.read();
    await reader.cancel();

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

    hasLoaded = true;
    resolve();

    await jest.runAllTimers();

    expect(rendered).toBe(false);

    expect(errors).toEqual([
      'The render was aborted by the server without a reason.',
    ]);
  });

  // @gate enablePostpone
  it('postpones when abort passes a postpone signal', async () => {
    const infinitePromise = new Promise(() => {});
    function Server() {
      return infinitePromise;
    }

    let postponed = null;
    let error = null;

    const controller = new AbortController();
    const stream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        <Suspense fallback="Loading...">
          <Server />
        </Suspense>,
        null,
        {
          onError(x) {
            error = x;
          },
          onPostpone(reason) {
            postponed = reason;
          },
          signal: controller.signal,
        },
      ),
    );

    try {
      React.unstable_postpone('testing postpone');
    } catch (reason) {
      controller.abort(reason);
    }

    const response = ReactServerDOMClient.createFromReadableStream(stream);

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

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <div>
          Shell: <Client />
        </div>,
      );
    });
    // We should have reserved the shell already. Which means that the Server
    // Component should've been a lazy component.
    expect(container.innerHTML).toContain('Shell:');
    expect(container.innerHTML).toContain('Loading...');
    expect(container.innerHTML).not.toContain('Not shown');

    expect(postponed).toBe('testing postpone');
    expect(error).toBe(null);
  });

  function passThrough(stream) {
    // Simulate more realistic network by splitting up and rejoining some chunks.
    // This lets us test that we don't accidentally rely on particular bounds of the chunks.
    return new ReadableStream({
      async start(controller) {
        const reader = stream.getReader();
        function push() {
          reader.read().then(({done, value}) => {
            if (done) {
              controller.close();
              return;
            }
            controller.enqueue(value);
            push();
            return;
          });
        }
        push();
      },
    });
  }

  // @gate enableFlightReadableStream
  it('should supports streaming ReadableStream with objects', async () => {
    const errors = [];
    let controller1;
    let controller2;
    const s1 = new ReadableStream({
      start(c) {
        controller1 = c;
      },
    });
    const s2 = new ReadableStream({
      start(c) {
        controller2 = c;
      },
    });
    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        {
          s1,
          s2,
        },
        {},
        {
          onError(x) {
            errors.push(x);
            return x;
          },
        },
      ),
    );
    const result = await ReactServerDOMClient.createFromReadableStream(
      passThrough(rscStream),
    );

    const reader1 = result.s1.getReader();
    const reader2 = result.s2.getReader();

    await serverAct(() => {
      controller1.enqueue({hello: 'world'});
      controller2.enqueue({hi: 'there'});
    });

    expect(await reader1.read()).toEqual({
      value: {hello: 'world'},
      done: false,
    });
    expect(await reader2.read()).toEqual({
      value: {hi: 'there'},
      done: false,
    });

    await serverAct(async () => {
      controller1.enqueue('text1');
      controller2.enqueue('text2');
      controller1.close();
    });

    expect(await reader1.read()).toEqual({
      value: 'text1',
      done: false,
    });
    expect(await reader1.read()).toEqual({
      value: undefined,
      done: true,
    });
    expect(await reader2.read()).toEqual({
      value: 'text2',
      done: false,
    });
    await serverAct(async () => {
      controller2.error('rejected');
    });
    let error = null;
    try {
      await reader2.read();
    } catch (x) {
      error = x;
    }
    expect(error.digest).toBe('rejected');
    expect(errors).toEqual(['rejected']);
  });

  // @gate enableFlightReadableStream
  it('should cancels the underlying ReadableStream when we are cancelled', async () => {
    let controller;
    let cancelReason;
    const s = new ReadableStream({
      start(c) {
        controller = c;
      },
      cancel(r) {
        cancelReason = r;
      },
    });
    let loggedReason;
    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        s,
        {},
        {
          onError(reason) {
            loggedReason = reason;
          },
        },
      ),
    );
    const reader = rscStream.getReader();
    controller.enqueue('hi');
    const reason = new Error('aborted');
    reader.cancel(reason);
    await reader.read();
    expect(cancelReason).toBe(reason);
    expect(loggedReason).toBe(reason);
  });

  // @gate enableFlightReadableStream
  it('should cancels the underlying ReadableStream when we abort', async () => {
    const errors = [];
    let controller;
    let cancelReason;
    const abortController = new AbortController();
    const s = new ReadableStream({
      start(c) {
        controller = c;
      },
      cancel(r) {
        cancelReason = r;
      },
    });

    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        s,
        {},
        {
          signal: abortController.signal,
          onError(x) {
            errors.push(x);
            return x.message;
          },
        },
      ),
    );
    const result = await ReactServerDOMClient.createFromReadableStream(
      passThrough(rscStream),
    );
    const reader = result.getReader();

    controller.enqueue('hi');

    await 0;

    const reason = new Error('aborted');
    abortController.abort(reason);

    // We should be able to read the part we already emitted before the abort
    expect(await reader.read()).toEqual({
      value: 'hi',
      done: false,
    });

    expect(cancelReason).toBe(reason);

    let error = null;
    try {
      await reader.read();
    } catch (x) {
      error = x;
    }
    expect(error.digest).toBe('aborted');
    expect(errors).toEqual([reason]);
  });

  // @gate enableFlightReadableStream
  it('should supports streaming AsyncIterables with objects', async () => {
    let resolve;
    const wait = new Promise(r => (resolve = r));
    const errors = [];
    const multiShotIterable = {
      async *[Symbol.asyncIterator]() {
        const next = yield {hello: 'A'};
        expect(next).toBe(undefined);
        await wait;
        yield {hi: 'B'};
        return 'C';
      },
    };
    const singleShotIterator = (async function* () {
      const next = yield {hello: 'D'};
      expect(next).toBe(undefined);
      await wait;
      yield {hi: 'E'};
      // eslint-disable-next-line no-throw-literal
      throw 'F';
    })();

    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        {
          multiShotIterable,
          singleShotIterator,
        },
        {},
        {
          onError(x) {
            errors.push(x);
            return x;
          },
        },
      ),
    );
    const result = await ReactServerDOMClient.createFromReadableStream(
      passThrough(rscStream),
    );

    const iterator1 = result.multiShotIterable[Symbol.asyncIterator]();
    const iterator2 = result.singleShotIterator[Symbol.asyncIterator]();

    expect(iterator1).not.toBe(result.multiShotIterable);
    expect(iterator2).toBe(result.singleShotIterator);

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

    await serverAct(() => {
      resolve();
    });

    expect(await iterator1.next()).toEqual({
      value: {hi: 'B'},
      done: false,
    });
    expect(await iterator2.next()).toEqual({
      value: {hi: 'E'},
      done: false,
    });
    expect(await iterator1.next()).toEqual({
      value: 'C', // Return value
      done: true,
    });
    expect(await iterator1.next()).toEqual({
      value: undefined,
      done: true,
    });

    let error = null;
    try {
      await iterator2.next();
    } catch (x) {
      error = x;
    }
    expect(error.digest).toBe('F');
    expect(errors).toEqual(['F']);

    // Multi-shot iterables should be able to do the same thing again
    const iterator3 = result.multiShotIterable[Symbol.asyncIterator]();

    expect(iterator3).not.toBe(iterator1);

    // We should be able to iterate over the iterable again and it should be
    // synchronously available using instrumented promises so that React can
    // rerender it synchronously.
    expect(iterator3.next().value).toEqual({
      value: {hello: 'A'},
      done: false,
    });
    expect(iterator3.next().value).toEqual({
      value: {hi: 'B'},
      done: false,
    });
    expect(iterator3.next().value).toEqual({
      value: 'C', // Return value
      done: true,
    });
    expect(iterator3.next().value).toEqual({
      value: undefined,
      done: true,
    });

    expect(() => iterator3.next('this is not allowed')).toThrow(
      'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
    );
  });

  // @gate enableFlightReadableStream
  it('should cancels the underlying AsyncIterable when we are cancelled', async () => {
    let resolve;
    const wait = new Promise(r => (resolve = r));
    let thrownReason;
    const iterator = (async function* () {
      try {
        await wait;
        yield 'a';
        yield 'b';
      } catch (x) {
        thrownReason = x;
      }
      yield 'c';
    })();
    let loggedReason;

    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        iterator,
        {},
        {
          onError(reason) {
            loggedReason = reason;
          },
        },
      ),
    );

    const reader = rscStream.getReader();

    const reason = new Error('aborted');
    reader.cancel(reason);
    await resolve();
    await reader.read();
    expect(thrownReason).toBe(reason);
    expect(loggedReason).toBe(reason);
  });

  // @gate enableFlightReadableStream
  it('should cancels the underlying AsyncIterable when we abort', async () => {
    const errors = [];
    const abortController = new AbortController();
    let resolve;
    const wait = new Promise(r => (resolve = r));
    let thrownReason;
    const iterator = (async function* () {
      try {
        yield 'a';
        await wait;
        yield 'b';
      } catch (x) {
        thrownReason = x;
      }
      yield 'c';
    })();
    const rscStream = await serverAct(() =>
      ReactServerDOMServer.renderToReadableStream(
        iterator,
        {},
        {
          signal: abortController.signal,
          onError(x) {
            errors.push(x);
            return x.message;
          },
        },
      ),
    );
    const result = await ReactServerDOMClient.createFromReadableStream(
      passThrough(rscStream),
    );

    const reason = new Error('aborted');
    abortController.abort(reason);

    await serverAct(() => {
      resolve();
    });

    // We should be able to read the part we already emitted before the abort
    expect(await result.next()).toEqual({
      value: 'a',
      done: false,
    });

    expect(thrownReason).toBe(reason);

    let error = null;
    try {
      await result.next();
    } catch (x) {
      error = x;
    }
    expect(error.digest).toBe('aborted');
    expect(errors).toEqual([reason]);
  });

  // @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.prerender(
          <App />,
          webpackMap,
        ),
      };
    });

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

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

    const response = ReactServerDOMClient.createFromReadableStream(
      passThrough(prelude),
    );
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);

    await act(() => {
      root.render(<ClientRoot response={response} />);
    });
    expect(container.innerHTML).toBe('<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.prerender(
          <App />,
          webpackMap,
          {
            signal: controller.signal,
            onError(err) {
              errors.push(err);
            },
          },
        ),
      };
    });

    controller.abort('boom');
    resolveGreeting();
    const {prelude} = await pendingResult;
    expect(errors).toEqual(['boom']);

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

    const response = ReactServerDOMClient.createFromReadableStream(
      passThrough(prelude),
    );
    const container = document.createElement('div');
    errors.length = 0;
    const root = ReactDOMClient.createRoot(container, {
      onUncaughtError(err) {
        errors.push(err);
      },
    });

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

    if (__DEV__) {
      expect(errors).toEqual([new Error('Connection closed.')]);
      expect(container.innerHTML).toBe('');
    } 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([]);
      expect(container.innerHTML).toBe('<div>loading...</div>');
    }
  });
});