'use strict';
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
import {
getVisibleChildren,
insertNodesAndExecuteScripts,
} from '../test-utils/FizzTestUtils';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let JSDOM;
let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
let SuspenseList;
let container;
let serverAct;
describe('ReactDOMFizzStaticBrowser', () => {
beforeEach(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
window.setTimeout = setTimeout;
patchMessageChannel();
serverAct = require('internal-test-utils').serverAct;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMFizzServer = require('react-dom/server.browser');
ReactDOMFizzStatic = require('react-dom/static.browser');
Suspense = React.Suspense;
SuspenseList = React.unstable_SuspenseList;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
if (typeof global.window.__restoreGlobalScope === 'function') {
global.window.__restoreGlobalScope();
}
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);
jest.runAllTimers();
}
async function readIntoNewDocument(stream) {
const content = await readContent(stream);
const jsdom = new JSDOM(
'<script>window.requestAnimationFrame = setTimeout;</script>' + content,
{
runScripts: 'dangerously',
},
);
const originalWindow = global.window;
const originalDocument = global.document;
const originalNavigator = global.navigator;
const originalNode = global.Node;
const originalAddEventListener = global.addEventListener;
const originalMutationObserver = global.MutationObserver;
global.window = jsdom.window;
global.document = global.window.document;
global.navigator = global.window.navigator;
global.Node = global.window.Node;
global.addEventListener = global.window.addEventListener;
global.MutationObserver = global.window.MutationObserver;
global.window.__restoreGlobalScope = () => {
global.window = originalWindow;
global.document = originalDocument;
global.navigator = originalNavigator;
global.Node = originalNode;
global.addEventListener = originalAddEventListener;
global.MutationObserver = originalMutationObserver;
};
}
async function readIntoCurrentDocument(stream) {
const content = await readContent(stream);
const temp = document.createElement('div');
temp.innerHTML = content;
await insertNodesAndExecuteScripts(temp, document.body, null);
jest.runAllTimers();
}
it('should call prerender', async () => {
const result = await serverAct(() =>
ReactDOMFizzStatic.prerender(<div>hello world</div>),
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
});
it('should emit DOCTYPE at the root of the document', async () => {
const result = await serverAct(() =>
ReactDOMFizzStatic.prerender(
<html>
<body>hello world</body>
</html>,
),
);
const prelude = await readContent(result.prelude);
if (gate(flags => flags.enableFizzBlockingRender)) {
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head><body>hello world<template id="_R_"></template></body></html>"`,
);
} else {
expect(prelude).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
);
}
});
it('should emit bootstrap script src at the end', async () => {
const result = await serverAct(() =>
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 id="_R_">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});
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 = serverAct(() =>
ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
),
);
await jest.runAllTimers();
hasLoaded = true;
await resolve();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
});
it('should reject the promise when an error is thrown at the root', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await serverAct(() =>
ReactDOMFizzStatic.prerender(
<div>
<Throw />
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
it('should reject the promise when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await serverAct(() =>
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]);
});
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const result = await serverAct(() =>
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]);
});
it('should be able to complete by aborting even if the promise never resolves', async () => {
const errors = [];
const controller = new AbortController();
let resultPromise;
await serverAct(() => {
resultPromise = ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
controller.abort();
const result = await resultPromise;
const prelude = await readContent(result.prelude);
expect(prelude).toContain('Loading');
expect(errors).toEqual(['This operation was aborted']);
});
it('should reject if aborting before the shell is complete and enableHalt is disabled', async () => {
const errors = [];
const controller = new AbortController();
const promise = serverAct(() =>
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']);
});
it('should resolve an empty prelude if aborting before the shell is complete', async () => {
const errors = [];
const controller = new AbortController();
const promise = serverAct(() =>
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 rejected = false;
let prelude;
try {
({prelude} = await promise);
} catch (error) {
rejected = true;
}
expect(rejected).toBe(false);
expect(errors).toEqual(['aborted for reasons']);
const content = await readContent(prelude);
expect(content).toBe('');
});
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 = serverAct(() =>
ReactDOMFizzStatic.prerender(
<div>
<App />
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
if (gate(flags => flags.enableHalt)) {
const {prelude} = await streamPromise;
const content = await readContent(prelude);
expect(errors).toEqual(['This operation was aborted']);
expect(content).toBe('');
} else {
let caughtError = null;
try {
await streamPromise;
} catch (error) {
caughtError = error;
}
expect(caughtError.message).toBe('This operation was aborted');
expect(errors).toEqual(['This operation was aborted']);
}
});
it('should reject if passing an already aborted signal and enableHalt is disabled', async () => {
const errors = [];
const controller = new AbortController();
const theReason = new Error('aborted for reasons');
controller.abort(theReason);
const promise = serverAct(() =>
ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
let caughtError = null;
try {
await promise;
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theReason);
expect(errors).toEqual(['aborted for reasons']);
});
it('should resolve an empty prelude 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 = serverAct(() =>
ReactDOMFizzStatic.prerender(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
),
);
let didThrow = false;
let prelude;
try {
({prelude} = await promise);
} catch (error) {
didThrow = true;
}
expect(didThrow).toBe(false);
expect(errors).toEqual(['aborted for reasons']);
const content = await readContent(prelude);
expect(content).toBe('');
});
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();
let resultPromise;
await serverAct(() => {
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']);
});
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();
let resultPromise;
await serverAct(() => {
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']);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const content = await serverAct(() =>
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);
});
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'});
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 serverAct(() =>
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 serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
await readIntoContainer(content);
expect(calledInit).toBe(true);
const link = document.querySelector(
'link[rel="stylesheet"][href="my-style2"]',
);
const event = document.createEvent('Events');
event.initEvent('load', true, true);
link.dispatchEvent(event);
await 0;
await 0;
jest.runAllTimers();
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>,
]);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
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 serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
const resumed = await serverAct(() =>
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>,
);
});
it('can postpone in fallback without postponing the tree', async () => {
function Postpone() {
React.unstable_postpone();
}
const lazyText = React.lazy(async () => {
await 0;
return {default: 'Hello'};
});
function App() {
return (
<div>
<Suspense fallback="Outer">
<Suspense fallback={<Postpone />}>{lazyText}</Suspense>
</Suspense>
</div>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
const resumed = await serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
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 serverAct(() =>
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 serverAct(() =>
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.",
]);
await readIntoContainer(resumed);
expect(getVisibleChildren(container)).toEqual([
<div>Loading...</div>,
<div>Loading...</div>,
]);
});
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 serverAct(() =>
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 = serverAct(() =>
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);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
});
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;
}
function Outer({children}) {
return children;
}
function App() {
return (
<div>
<Outer>
<Delay>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</Delay>
</Outer>
</div>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
prerendering = false;
const resumedPromise = serverAct(() =>
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>);
});
it('emits an empty prelude and resumes at the root if we postpone in the shell', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}
function App() {
return (
<html lang="en">
<body>
<link rel="stylesheet" href="my-style" precedence="high" />
<Postpone />
</body>
</html>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
expect(await readContent(prerendered.prelude)).toBe('');
const content = await serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#_R_" blocking="render"/>'
: '') +
'</head>' +
'<body>Hello' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="_R_"></template>'
: '') +
'</body></html>',
);
});
it('emits an empty prelude if we have not rendered html or head tags yet', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<html lang="en">
<body>Hello</body>
</html>
);
}
function App() {
return (
<>
<link rel="stylesheet" href="my-style" precedence="high" />
<Postpone />
</>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
expect(await readContent(prerendered.prelude)).toBe('');
const content = await serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'<link rel="expect" href="#_R_" blocking="render"/></head>' +
'<body>Hello<template id="_R_"></template></body></html>',
);
});
it('emits an empty prelude if a postpone in a promise in the shell', 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 (
<html>
<link rel="stylesheet" href="my-style" precedence="high" />
<body>
<div>
<Lazy />
</div>
</body>
</html>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
expect(await readContent(prerendered.prelude)).toBe('');
const content = await serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
expect(await readContent(content)).toBe(
'<!DOCTYPE html><html><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'<link rel="expect" href="#_R_" blocking="render"/></head>' +
'<body><div>Hello</div><template id="_R_"></template></body></html>',
);
});
it('does not emit preloads during resume for Resources preloaded through onHeaders', async () => {
let prerendering = true;
let hasLoaded = false;
let resolve;
const promise = new Promise(r => (resolve = r));
function WaitIfResuming({children}) {
if (!prerendering && !hasLoaded) {
throw promise;
}
return children;
}
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return null;
}
let headers;
function onHeaders(x) {
headers = x;
}
function App() {
ReactDOM.preload('image', {as: 'image', fetchPriority: 'high'});
return (
<html>
<body>
hello
<Suspense fallback={null}>
<WaitIfResuming>
world
<link rel="stylesheet" href="style" precedence="default" />
</WaitIfResuming>
</Suspense>
<Postpone />
</body>
</html>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />, {
onHeaders,
}),
);
expect(prerendered.postponed).not.toBe(null);
prerendering = false;
expect(await readContent(prerendered.prelude)).toBe('');
expect(headers).toEqual(
new Headers({
Link: `
<image>; rel=preload; as="image"; fetchpriority="high",
<style>; rel=preload; as="style"
`
.replaceAll('\n', '')
.trim(),
}),
);
const content = await serverAct(() =>
ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
),
);
const decoder = new TextDecoder();
const reader = content.getReader();
let {value, done} = await reader.read();
let result = decoder.decode(value, {stream: true});
expect(result).toBe(
'<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head>' +
'<body>hello<!--$?--><template id="B:1"></template><!--/$--><script id="_R_">requestAnimationFrame(function(){$RT=performance.now()});</script>',
);
await 1;
hasLoaded = true;
await serverAct(resolve);
while (true) {
({value, done} = await reader.read());
if (done) {
result += decoder.decode(value);
break;
}
result += decoder.decode(value, {stream: true});
}
const instructionIndex = result.indexOf('$RX');
expect(instructionIndex > -1).toBe(true);
const slice = result.slice(0, instructionIndex + '$RX'.length);
expect(slice).toBe(
'<!DOCTYPE html><html><head><link rel="expect" href="#_R_" blocking="render"/></head>' +
'<body>hello<!--$?--><template id="B:1"></template><!--/$--><script id="_R_">requestAnimationFrame(function(){$RT=performance.now()});</script>' +
'<div hidden id="S:1">world<!-- --></div><script>$RX',
);
});
it('logs an error if onHeaders throws but continues the prerender', async () => {
const errors = [];
function onError(error) {
errors.push(error.message);
}
function onHeaders(x) {
throw new Error('bad onHeaders');
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<div>hello</div>, {
onHeaders,
onError,
}),
);
expect(prerendered.postponed).toBe(
gate(flags => flags.enableHalt || flags.enablePostpone)
? null
: undefined,
);
expect(errors).toEqual(['bad onHeaders']);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>hello</div>);
});
it('does not bootstrap again in a resume if it bootstraps', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return null;
}
function App() {
return (
<html>
<body>
<Suspense fallback="loading...">
<Postpone />
hello
</Suspense>
</body>
</html>
);
}
let inits = 0;
jest.mock(
'init.js',
() => {
inits++;
},
{virtual: true},
);
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />, {
bootstrapScripts: ['init.js'],
}),
);
const postponedSerializedState = JSON.stringify(prerendered.postponed);
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual([
<link rel="preload" href="init.js" fetchpriority="low" as="script" />,
'loading...',
]);
expect(inits).toBe(1);
jest.resetModules();
jest.mock(
'init.js',
() => {
inits++;
},
{virtual: true},
);
prerendering = false;
const content = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedSerializedState)),
);
await readIntoContainer(content);
expect(inits).toBe(1);
expect(getVisibleChildren(container)).toEqual([
<link rel="preload" href="init.js" fetchpriority="low" as="script" />,
'hello',
]);
});
it('can render a deep list of single components where one postpones', async () => {
let isPrerendering = true;
function Outer({children}) {
return children;
}
function Middle({children}) {
return children;
}
function Inner() {
if (isPrerendering) {
React.unstable_postpone();
}
return 'hello';
}
function App() {
return (
<div>
<Suspense fallback="loading...">
<Outer>
<Middle>
<Inner />
</Middle>
</Outer>
</Suspense>
</div>
);
}
const prerendered = await serverAct(() =>
ReactDOMFizzStatic.prerender(<App />),
);
const postponedState = JSON.stringify(prerendered.postponed);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>loading...</div>);
isPrerendering = false;
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
);
await readIntoContainer(dynamic);
expect(getVisibleChildren(container)).toEqual(<div>hello</div>);
});
it('can resume render of a prerender', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return (
<Suspense fallback="Loading B">
<ComponentB />
</Suspense>
);
}
async function ComponentB() {
await promiseB;
return 'Hello';
}
function App() {
return (
<div>
<Suspense fallback="Loading A">
<ComponentA />
</Suspense>
</div>
);
}
const controller = new AbortController();
let pendingResult;
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
});
});
controller.abort();
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading A</div>);
await resolveA();
expect(prerendered.postponed).not.toBe(null);
const controller2 = new AbortController();
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
<App />,
JSON.parse(postponedState),
{
signal: controller2.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
controller2.abort();
const prerendered2 = await pendingResult;
const postponedState2 = JSON.stringify(prerendered2.postponed);
await readIntoContainer(prerendered2.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading B</div>);
await resolveB();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState2)),
);
await readIntoContainer(dynamic);
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
it('can prerender a preamble', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return (
<Suspense fallback="Loading B">
<ComponentB />
</Suspense>
);
}
async function ComponentB() {
await promiseB;
return 'Hello';
}
function App() {
return (
<Suspense>
<html data-x="">
<body data-x="">
<Suspense fallback="Loading A">
<ComponentA />
</Suspense>
</body>
</html>
</Suspense>
);
}
const controller = new AbortController();
let pendingResult;
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
});
});
controller.abort();
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
await readIntoNewDocument(prerendered.prelude);
expect(getVisibleChildren(document)).toEqual(
<html data-x="">
<head />
<body data-x="">Loading A</body>
</html>,
);
await resolveA();
expect(prerendered.postponed).not.toBe(null);
const controller2 = new AbortController();
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
<App />,
JSON.parse(postponedState),
{
signal: controller2.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
controller2.abort();
const prerendered2 = await pendingResult;
const postponedState2 = JSON.stringify(prerendered2.postponed);
await readIntoCurrentDocument(prerendered2.prelude);
expect(getVisibleChildren(document)).toEqual(
<html data-x="">
<head />
<body data-x="">Loading B</body>
</html>,
);
await resolveB();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState2)),
);
await readIntoCurrentDocument(dynamic);
expect(getVisibleChildren(document)).toEqual(
<html data-x="">
<head />
<body data-x="">Hello</body>
</html>,
);
});
it('can suspend inside <head> tag', async () => {
const promise = new Promise(() => {});
function App() {
return (
<html>
<head>
<Suspense fallback={<meta itemProp="" content="fallback" />}>
<Metadata />
</Suspense>
</head>
<body>
<div>hello</div>
</body>
</html>
);
}
function Metadata() {
React.use(promise);
return <meta itemProp="" content="primary" />;
}
const controller = new AbortController();
let pendingResult;
const errors = [];
await serverAct(() => {
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError: e => {
errors.push(e.message);
},
});
});
controller.abort(new Error('boom'));
const prerendered = await pendingResult;
await readIntoNewDocument(prerendered.prelude);
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<meta itemprop="" content="fallback" />
</head>
<body>
<div>hello</div>
</body>
</html>,
);
expect(errors).toEqual(['boom']);
});
it('will render fallback Document when erroring a boundary above the body', async () => {
let isPrerendering = true;
const promise = new Promise(() => {});
function Boom() {
if (isPrerendering) {
React.use(promise);
}
throw new Error('Boom!');
}
function App() {
return (
<Suspense
fallback={
<html data-error-html="">
<body data-error-body="">
<span>hello error</span>
</body>
</html>
}>
<html data-content-html="">
<body data-content-body="">
<Boom />
<span>hello world</span>
</body>
</html>
</Suspense>
);
}
const controller = new AbortController();
let pendingResult;
const errors = [];
await serverAct(() => {
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError: e => {
errors.push(e.message);
},
});
});
controller.abort();
const prerendered = await pendingResult;
expect(errors).toEqual(['This operation was aborted']);
const content = await readContent(prerendered.prelude);
expect(content).toBe('');
isPrerendering = false;
const postponedState = JSON.stringify(prerendered.postponed);
const resumeErrors = [];
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState), {
onError: e => {
resumeErrors.push(e.message);
},
}),
);
expect(resumeErrors).toEqual(['Boom!']);
await readIntoNewDocument(dynamic);
expect(getVisibleChildren(document)).toEqual(
<html data-error-html="">
<head />
<body data-error-body="">
<span>hello error</span>
</body>
</html>,
);
});
it('can omit a preamble with an empty shell if no preamble is ready when prerendering finishes', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return (
<Suspense fallback="Loading B">
<ComponentB />
</Suspense>
);
}
async function ComponentB() {
await promiseB;
return 'Hello';
}
function App() {
return (
<Suspense>
<html data-x="">
<body data-x="">
<ComponentA />
</body>
</html>
</Suspense>
);
}
const controller = new AbortController();
let pendingResult;
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
});
});
controller.abort();
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
const content = await readContent(prerendered.prelude);
expect(content).toBe('');
await resolveA();
expect(prerendered.postponed).not.toBe(null);
const controller2 = new AbortController();
await serverAct(async () => {
pendingResult = ReactDOMFizzStatic.resumeAndPrerender(
<App />,
JSON.parse(postponedState),
{
signal: controller2.signal,
onError(x) {
errors.push(x.message);
},
},
);
});
controller2.abort();
const prerendered2 = await pendingResult;
const postponedState2 = JSON.stringify(prerendered2.postponed);
await readIntoNewDocument(prerendered2.prelude);
expect(getVisibleChildren(document)).toEqual(
<html data-x="">
<head />
<body data-x="">Loading B</body>
</html>,
);
await resolveB();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState2)),
);
await readIntoCurrentDocument(dynamic);
expect(getVisibleChildren(document)).toEqual(
<html data-x="">
<head />
<body data-x="">Hello</body>
</html>,
);
});
it('can resume a partially prerendered SuspenseList', async () => {
const errors = [];
let resolveA;
const promiseA = new Promise(r => (resolveA = r));
let resolveB;
const promiseB = new Promise(r => (resolveB = r));
async function ComponentA() {
await promiseA;
return 'A';
}
async function ComponentB() {
await promiseB;
return 'B';
}
function App() {
return (
<div>
<SuspenseList revealOrder="forwards" tail="visible">
<Suspense fallback="Loading A">
<ComponentA />
</Suspense>
<Suspense fallback="Loading B">
<ComponentB />
</Suspense>
<Suspense fallback="Loading C">C</Suspense>
<Suspense fallback="Loading D">D</Suspense>
</SuspenseList>
</div>
);
}
const controller = new AbortController();
const pendingResult = serverAct(() =>
ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
}),
);
await serverAct(() => {
controller.abort();
});
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(
<div>
{'Loading A'}
{'Loading B'}
{'Loading C'}
{'Loading D'}
</div>,
);
expect(prerendered.postponed).not.toBe(null);
await resolveA();
await resolveB();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
);
await readIntoContainer(dynamic);
expect(getVisibleChildren(container)).toEqual(
<div>
{'A'}
{'B'}
{'C'}
{'D'}
</div>,
);
});
});