Files
react/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
T
Sebastian Markbåge 09fbee89d6 [Fizz] Don't pop the replay stack if we've already rendered past an element (#27513)
This is the same problem as we had with keyPath before where if the
element itself suspends, we have to restore the replay node to what it
was before, however, if something below the element suspends we
shouldn't pop it because that will pop it back up the stack.

Instead of passing replay as an argument to every renderElement
function, I use a hack to compare if the node is still the same as the
one we tried to render, then that means we haven't stepped down into the
child yet. Maybe this is not quite correct because in theory you could
have a recursive node that just renders itself over and over until some
context bails out.

This solves an issue where if you suspended in an element it would retry
trying to replay from that element but using the postponed state from
the root.
2023-10-13 09:21:21 -04:00

1191 lines
30 KiB
JavaScript

/**
* 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 {
getVisibleChildren,
insertNodesAndExecuteScripts,
} from '../test-utils/FizzTestUtils';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
let container;
describe('ReactDOMFizzStaticBrowser', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMFizzServer = require('react-dom/server.browser');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static.browser');
}
Suspense = React.Suspense;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
const theError = new Error('This is an error');
function Throw() {
throw theError;
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}
function concat(streamA, streamB) {
const readerA = streamA.getReader();
const readerB = streamB.getReader();
return new ReadableStream({
start(controller) {
function readA() {
readerA.read().then(({done, value}) => {
if (done) {
readB();
return;
}
controller.enqueue(value);
readA();
});
}
function readB() {
readerB.read().then(({done, value}) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
readB();
});
}
readA();
},
});
}
async function readContent(stream) {
const reader = stream.getReader();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return content;
}
content += Buffer.from(value).toString('utf8');
}
}
async function readIntoContainer(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
result += Buffer.from(value).toString('utf8');
}
const temp = document.createElement('div');
temp.innerHTML = result;
await insertNodesAndExecuteScripts(temp, container, null);
}
// @gate experimental
it('should call prerender', async () => {
const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
});
// @gate experimental
it('should emit DOCTYPE at the root of the document', async () => {
const result = await ReactDOMFizzStatic.prerender(
<html>
<body>hello world</body>
</html>,
);
const prelude = await readContent(result.prelude);
if (gate(flags => flags.enableFloat)) {
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
} else {
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><body>hello world</body></html>"`,
);
}
});
// @gate experimental
it('should emit bootstrap script src at the end', async () => {
const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>, {
bootstrapScriptContent: 'INIT();',
bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
});
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});
// @gate experimental
it('emits all HTML as one unit', async () => {
let hasLoaded = false;
let resolve;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
return 'Done';
}
const resultPromise = ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
);
await jest.runAllTimers();
// Resolve the loading.
hasLoaded = true;
await resolve();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<div><!--$-->Done<!-- --><!--/$--></div>"`,
);
});
// @gate experimental
it('should reject the promise when an error is thrown at the root', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await ReactDOMFizzStatic.prerender(
<div>
<Throw />
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should reject the promise when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<Throw />}>
<InfiniteSuspend />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const result = await ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<Throw />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should be able to complete by aborting even if the promise never resolves', async () => {
const errors = [];
const controller = new AbortController();
const resultPromise = ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
await jest.runAllTimers();
controller.abort();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(errors).toEqual(['The operation was aborted.']);
});
// @gate experimental
it('should reject if aborting before the shell is complete', async () => {
const errors = [];
const controller = new AbortController();
const promise = ReactDOMFizzStatic.prerender(
<div>
<InfiniteSuspend />
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
await jest.runAllTimers();
const theReason = new Error('aborted for reasons');
controller.abort(theReason);
let caughtError = null;
try {
await promise;
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theReason);
expect(errors).toEqual(['aborted for reasons']);
});
// @gate experimental
it('should be able to abort before something suspends', async () => {
const errors = [];
const controller = new AbortController();
function App() {
controller.abort();
return (
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
);
}
const streamPromise = ReactDOMFizzStatic.prerender(
<div>
<App />
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
let caughtError = null;
try {
await streamPromise;
} catch (error) {
caughtError = error;
}
expect(caughtError.message).toBe('The operation was aborted.');
expect(errors).toEqual(['The operation was aborted.']);
});
// @gate experimental
it('should reject if passing an already aborted signal', async () => {
const errors = [];
const controller = new AbortController();
const theReason = new Error('aborted for reasons');
controller.abort(theReason);
const promise = ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
// Technically we could still continue rendering the shell but currently the
// semantics mean that we also abort any pending CPU work.
let caughtError = null;
try {
await promise;
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theReason);
expect(errors).toEqual(['aborted for reasons']);
});
// @gate experimental
it('supports custom abort reasons with a string', async () => {
const promise = new Promise(r => {});
function Wait() {
throw promise;
}
function App() {
return (
<div>
<p>
<Suspense fallback={'p'}>
<Wait />
</Suspense>
</p>
<span>
<Suspense fallback={'span'}>
<Wait />
</Suspense>
</span>
</div>
);
}
const errors = [];
const controller = new AbortController();
const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x);
return 'a digest';
},
});
controller.abort('foobar');
await resultPromise;
expect(errors).toEqual(['foobar', 'foobar']);
});
// @gate experimental
it('supports custom abort reasons with an Error', async () => {
const promise = new Promise(r => {});
function Wait() {
throw promise;
}
function App() {
return (
<div>
<p>
<Suspense fallback={'p'}>
<Wait />
</Suspense>
</p>
<span>
<Suspense fallback={'span'}>
<Wait />
</Suspense>
</span>
</div>
);
}
const errors = [];
const controller = new AbortController();
const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
return 'a digest';
},
});
controller.abort(new Error('uh oh'));
await resultPromise;
expect(errors).toEqual(['uh oh', 'uh oh']);
});
// @gate enablePostpone
it('supports postponing in prerender and resuming later', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return ['Hello', 'World'];
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>{['Hello', 'World']}</div>,
);
});
// @gate enablePostpone
it('supports postponing in prerender and resuming with a prefix', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'World';
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
Hello
<Postpone />
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>{['Hello', 'World']}</div>,
);
});
// @gate enablePostpone
it('supports postponing in lazy in prerender and resuming later', async () => {
let prerendering = true;
const Hole = React.lazy(async () => {
React.unstable_postpone();
});
function App() {
return (
<div>
<Suspense fallback="Loading...">
Hi
{prerendering ? Hole : 'Hello'}
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>
{'Hi'}
{'Hello'}
</div>,
);
});
// @gate enablePostpone
it('supports postponing in a nested array', async () => {
let prerendering = true;
const Hole = React.lazy(async () => {
React.unstable_postpone();
});
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
Hi
{[<Postpone key="key" />, prerendering ? Hole : 'World']}
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>{['Hi', 'Hello', 'World']}</div>,
);
});
// @gate enablePostpone
it('supports postponing in lazy as a direct child', async () => {
let prerendering = true;
const Hole = React.lazy(async () => {
React.unstable_postpone();
});
function Postpone() {
return prerendering ? Hole : 'Hello';
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Postpone key="key" />
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
// @gate enablePostpone
it('only emits end tags once when resuming', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
return (
<html>
<body>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</body>
</html>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
const html = await readContent(concat(prerendered.prelude, content));
const htmlEndTags = /<\/html\s*>/gi;
const bodyEndTags = /<\/body\s*>/gi;
expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
});
// @gate enablePostpone
it('can prerender various hoistables and deduped resources', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<>
<link rel="stylesheet" href="my-style2" precedence="low" />
<link rel="stylesheet" href="my-style1" precedence="high" />
<style precedence="high" href="my-style3">
style
</style>
<img src="my-img" />
</>
);
}
function App() {
ReactDOM.preconnect('example.com');
ReactDOM.preload('my-font', {as: 'font', type: 'font/woff2'});
ReactDOM.preload('my-style0', {as: 'style'});
// This should transfer the props in to the style that loads later.
ReactDOM.preload('my-style2', {
as: 'style',
crossOrigin: 'use-credentials',
});
return (
<div>
<Suspense fallback="Loading...">
<link rel="stylesheet" href="my-style1" precedence="high" />
<img src="my-img" />
<Postpone />
</Suspense>
<title>Hello World</title>
</div>
);
}
let calledInit = false;
jest.mock(
'init.js',
() => {
calledInit = true;
},
{virtual: true},
);
const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
bootstrapScripts: ['init.js'],
});
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>Loading...</div>,
]);
prerendering = false;
const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(content);
expect(calledInit).toBe(true);
// Dispatch load event to injected stylesheet
const link = document.querySelector(
'link[rel="stylesheet"][href="my-style2"]',
);
const event = document.createEvent('Events');
event.initEvent('load', true, true);
link.dispatchEvent(event);
// Wait for the instruction microtasks to flush.
await 0;
await 0;
expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<style data-href="my-style3" data-precedence="high">
style
</style>,
<link
crossorigin="use-credentials"
data-precedence="low"
href="my-style2"
rel="stylesheet"
/>,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>
<img src="my-img" />
<img src="my-img" />
</div>,
]);
});
// @gate enablePostpone
it('can postpone a boundary after it has already been added', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
<Postpone />
<Postpone />
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>{['Hello', 'Hello', 'Hello']}</div>,
);
});
// @gate enablePostpone
it('can postpone in fallback', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
const Lazy = React.lazy(async () => {
await 0;
return {default: Postpone};
});
function App() {
return (
<div>
<Suspense fallback="Outer">
<Suspense fallback={<Postpone />}>
<Postpone /> World
</Suspense>
<Suspense fallback={<Postpone />}>
<Lazy />
</Suspense>
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(
<div>
{'Hello'}
{' World'}
{'Hello'}
</div>,
);
});
// @gate enablePostpone
it('can postpone in fallback without postponing the tree', async () => {
function Postpone() {
React.unstable_postpone();
}
const lazyText = React.lazy(async () => {
await 0; // causes the fallback to start work
return {default: 'Hello'};
});
function App() {
return (
<div>
<Suspense fallback="Outer">
<Suspense fallback={<Postpone />}>{lazyText}</Suspense>
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
// TODO: This should actually be null because we should've been able to fully
// resolve the render on the server eventually, even though the fallback postponed.
// So we should not need to resume.
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
// @gate enablePostpone
it('errors if the replay does not line up', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function Wrapper({children}) {
return children;
}
const lazySpan = React.lazy(async () => {
await 0;
return {default: <span />};
});
function App() {
const children = (
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
);
return (
<>
<div>{prerendering ? <Wrapper>{children}</Wrapper> : children}</div>
<div>
{prerendering ? (
<Suspense fallback="Loading...">
<div>
<Postpone />
</div>
</Suspense>
) : (
lazySpan
)}
</div>
</>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual([
<div>Loading...</div>,
<div>Loading...</div>,
]);
prerendering = false;
const errors = [];
const resumed = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
errors.push(x.message);
},
},
);
expect(errors).toEqual([
'Expected the resume to render <Wrapper> in this slot but instead it rendered <Suspense>. ' +
"The tree doesn't match so React will fallback to client rendering.",
'Expected the resume to render <Suspense> in this slot but instead it rendered <span>. ' +
"The tree doesn't match so React will fallback to client rendering.",
]);
// TODO: Test the component stack but we don't expose it to the server yet.
await readIntoContainer(resumed);
// Client rendered
expect(getVisibleChildren(container)).toEqual([
<div>Loading...</div>,
<div>Loading...</div>,
]);
});
// @gate enablePostpone
it('can abort the resume', async () => {
let prerendering = true;
const infinitePromise = new Promise(() => {});
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
if (!prerendering) {
React.use(infinitePromise);
}
return (
<div>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
prerendering = false;
const controller = new AbortController();
const errors = [];
const resumedPromise = ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
signal: controller.signal,
onError(x) {
errors.push(x);
},
},
);
controller.abort('abort');
const resumed = await resumedPromise;
await resumed.allReady;
expect(errors).toEqual(['abort']);
await readIntoContainer(resumed);
// Client rendered
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
});
// @gate enablePostpone
it('can suspend in a replayed component several layers deep', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
let resolve;
const promise = new Promise(r => (resolve = r));
function Delay({children}) {
if (!prerendering) {
React.use(promise);
}
return children;
}
// This wrapper will cause us to do one destructive render past this.
function Outer({children}) {
return children;
}
function App() {
return (
<div>
<Outer>
<Delay>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</Delay>
</Outer>
</div>
);
}
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
prerendering = false;
const resumedPromise = ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await jest.runAllTimers();
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await resolve();
await readIntoContainer(await resumedPromise);
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
});