Files
react/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
T
Sebastian Markbåge b94603b955 [Fizz] Gate rel="expect" behind enableFizzBlockingRender (#33183)
Enabled in experimental channel.

We know this is critical semantics to enforce at the HTML level since if
you don't then you can't add explicit boundaries after the fact.
However, this might have to go in a major release to allow for
upgrading.
2025-05-13 10:17:53 -04:00

3145 lines
86 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 {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
import {Readable} from 'stream';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let act;
let use;
let clientExports;
let clientExportsESM;
let clientModuleError;
let webpackMap;
let Stream;
let FlightReact;
let React;
let FlightReactDOM;
let ReactDOMClient;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let ReactDOMFizzServer;
let ReactDOMStaticServer;
let Suspense;
let ErrorBoundary;
let JSDOM;
let ReactServerScheduler;
let reactServerAct;
let assertConsoleErrorDev;
describe('ReactFlightDOM', () => {
beforeEach(() => {
// For this first reset we are going to load the dom-node version of react-server-dom-webpack/server
// This can be thought of as essentially being the React Server Components scope with react-server
// condition
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
ReactServerScheduler = require('scheduler');
patchSetImmediate(ReactServerScheduler);
reactServerAct = require('internal-test-utils').act;
// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
FlightReact = require('react');
FlightReactDOM = require('react-dom');
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node.unbundled'),
);
if (__EXPERIMENTAL__) {
jest.mock('react-server-dom-webpack/static', () =>
require('react-server-dom-webpack/static.node.unbundled'),
);
}
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
clientExportsESM = WebpackMock.clientExportsESM;
clientModuleError = WebpackMock.clientModuleError;
webpackMap = WebpackMock.webpackMap;
ReactServerDOMServer = require('react-server-dom-webpack/server');
if (__EXPERIMENTAL__) {
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
}
// This reset is to load modules for the SSR/Browser scope.
jest.unmock('react-server-dom-webpack/server');
__unmockReact();
jest.resetModules();
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
Stream = require('stream');
React = require('react');
use = React.use;
Suspense = React.Suspense;
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server.node');
ReactDOMStaticServer = require('react-dom/static.node');
ReactServerDOMClient = require('react-server-dom-webpack/client');
ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
};
});
async function serverAct(callback) {
let maybePromise;
await reactServerAct(() => {
maybePromise = callback();
if (maybePromise && typeof maybePromise.catch === 'function') {
maybePromise.catch(() => {});
}
});
return maybePromise;
}
async function readInto(
container: Document | HTMLElement,
stream: ReadableStream,
) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
content += decoder.decode();
break;
}
content += decoder.decode(value, {stream: true});
}
if (container.nodeType === 9 /* DOCUMENT */) {
const doc = new JSDOM(content).window.document;
container.documentElement.innerHTML = doc.documentElement.innerHTML;
while (container.documentElement.attributes.length > 0) {
container.documentElement.removeAttribute(
container.documentElement.attributes[0].name,
);
}
const attrs = doc.documentElement.attributes;
for (let i = 0; i < attrs.length; i++) {
container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
}
} else {
container.innerHTML = content;
}
}
function getTestStream() {
const writable = new Stream.PassThrough();
const readable = new ReadableStream({
start(controller) {
writable.on('data', chunk => {
controller.enqueue(chunk);
});
writable.on('end', () => {
controller.close();
});
},
});
return {
readable,
writable,
};
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}
function getMeaningfulChildren(element) {
const children = [];
let node = element.firstChild;
while (node) {
if (node.nodeType === 1) {
if (
// some tags are ambiguous and might be hidden because they look like non-meaningful children
// so we have a global override where if this data attribute is included we also include the node
node.hasAttribute('data-meaningful') ||
(node.tagName === 'SCRIPT' &&
node.hasAttribute('src') &&
node.hasAttribute('async')) ||
(node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden') &&
// Ignore the render blocking expect
(node.getAttribute('rel') !== 'expect' ||
node.getAttribute('blocking') !== 'render'))
) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
// We assume this is a React added ID that's a non-visual implementation detail.
continue;
}
props[attributes[i].name] = attributes[i].value;
}
props.children = getMeaningfulChildren(node);
children.push(React.createElement(node.tagName.toLowerCase(), props));
}
} else if (node.nodeType === 3) {
children.push(node.data);
}
node = node.nextSibling;
}
return children.length === 0
? undefined
: children.length === 1
? children[0]
: children;
}
it('should resolve HTML using Node streams', async () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}
function App() {
const model = {
html: <HTML />,
};
return model;
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const model = await response;
expect(model).toEqual({
html: (
<div>
<span>hello</span>
<span>world</span>
</div>
),
});
});
it('should resolve the root', async () => {
// Model
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}
function RootModel() {
return {
html: <HTML />,
};
}
// View
function Message({response}) {
return <section>{use(response).html}</section>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Message response={response} />
</Suspense>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe(
'<section><div><span>hello</span><span>world</span></div></section>',
);
});
it('should not get confused by $', async () => {
// Model
function RootModel() {
return {text: '$1'};
}
// View
function Message({response}) {
return <p>{use(response).text}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Message response={response} />
</Suspense>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>$1</p>');
});
it('should not get confused by @', async () => {
// Model
function RootModel() {
return {text: '@div'};
}
// View
function Message({response}) {
return <p>{use(response).text}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Message response={response} />
</Suspense>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>@div</p>');
});
it('should be able to esm compat test module references', async () => {
const ESMCompatModule = {
__esModule: true,
default: function ({greeting}) {
return greeting + ' World';
},
hi: 'Hello',
};
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
function interopWebpack(obj) {
// Basically what Webpack's ESM interop feature testing does.
if (typeof obj === 'object' && obj.__esModule) {
return obj;
}
return Object.assign({default: obj}, obj);
}
const {default: Component, hi} = interopWebpack(
clientExports(ESMCompatModule),
);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<Component greeting={hi} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Hello World</p>');
});
it('should be able to render a named component export', async () => {
const Module = {
Component: function ({greeting}) {
return greeting + ' World';
},
};
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const {Component} = clientExports(Module);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<Component greeting={'Hello'} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Hello World</p>');
});
it('should be able to render a module split named component export', async () => {
const Module = {
// This gets split into a separate module from the original one.
split: function ({greeting}) {
return greeting + ' World';
},
};
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const {split: Component} = clientExports(Module);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<Component greeting={'Hello'} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Hello World</p>');
});
it('should unwrap async module references', async () => {
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
return 'Async: ' + text;
});
const AsyncModule2 = Promise.resolve({
exportName: 'Module',
});
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const AsyncModuleRef = await clientExports(AsyncModule);
const AsyncModuleRef2 = await clientExports(AsyncModule2);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef text={AsyncModuleRef2.exportName} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Async: Module</p>');
});
it('should unwrap async module references using use', async () => {
const AsyncModule = Promise.resolve('Async Text');
function Print({response}) {
return use(response);
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const AsyncModuleRef = clientExports(AsyncModule);
function ServerComponent() {
const text = FlightReact.use(AsyncModuleRef);
return <p>{text}</p>;
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Async Text</p>');
});
it('should unwrap async ESM module references', async () => {
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
return 'Async: ' + text;
});
const AsyncModule2 = Promise.resolve({
exportName: 'Module',
});
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const AsyncModuleRef = await clientExportsESM(AsyncModule);
const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef text={AsyncModuleRef2.exportName} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Async: Module</p>');
});
it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => {
const AsyncModule = Promise.resolve(function AsyncModule() {
return 'This should not be rendered';
});
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<ErrorBoundary
fallback={error => (
<p>
{__DEV__ ? error.message + ' + ' : null}
{error.digest}
</p>
)}>
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
</ErrorBoundary>
);
}
const AsyncModuleRef = await clientExportsESM(AsyncModule, {
forceClientModuleProxy: true,
});
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef />,
webpackMap,
{
onError(error) {
return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;
expect(container.innerHTML).toBe(
__DEV__
? `<p>${errorMessage} + a dev digest</p>`
: `<p>digest(${errorMessage})</p>`,
);
});
it('should be able to import a name called "then"', async () => {
const thenExports = {
then: function then() {
return 'and then';
},
};
function Print({response}) {
return <p>{use(response)}</p>;
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const ThenRef = clientExports(thenExports).then;
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<ThenRef />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>and then</p>');
});
it('throws when accessing a member below the client exports', () => {
const ClientModule = clientExports({
Component: {deep: 'thing'},
});
function dotting() {
return ClientModule.Component.deep;
}
expect(dotting).toThrowError(
'Cannot access Component.deep on the server. ' +
'You cannot dot into a client module from a server component. ' +
'You can only pass the imported name through.',
);
});
it('throws when await a client module prop of client exports', async () => {
const ClientModule = clientExports({
Component: {deep: 'thing'},
});
async function awaitExport() {
const mod = await ClientModule;
return await Promise.resolve(mod.Component);
}
await expect(awaitExport()).rejects.toThrowError(
`Cannot await or return from a thenable. ` +
`You cannot await a client module from a server component.`,
);
});
it('throws when accessing a symbol prop from client exports', () => {
const symbol = Symbol('test');
const ClientModule = clientExports({
Component: {deep: 'thing'},
});
function read() {
return ClientModule[symbol];
}
expect(read).toThrowError(
'Cannot read Symbol exports. ' +
'Only named exports are supported on a client module imported on the server.',
);
});
it('does not throw when toString:ing client exports', () => {
const ClientModule = clientExports({
Component: {deep: 'thing'},
});
expect(Object.prototype.toString.call(ClientModule)).toBe(
'[object Object]',
);
expect(Object.prototype.toString.call(ClientModule.Component)).toBe(
'[object Function]',
);
});
it('does not throw when React inspects any deep props', () => {
const ClientModule = clientExports({
Component: function () {},
});
<ClientModule.Component key="this adds instrumentation" />;
});
it('throws when accessing a Context.Provider below the client exports', () => {
const Context = React.createContext();
const ClientModule = clientExports({
Context,
});
function dotting() {
return ClientModule.Context.Provider;
}
expect(dotting).toThrowError(
`Cannot render a Client Context Provider on the Server. ` +
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`,
);
});
it('should progressively reveal server components', async () => {
let reportedErrors = [];
// Client Components
function MyErrorBoundary({children}) {
return (
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
{children}
</ErrorBoundary>
);
}
// Model
function Text({children}) {
return children;
}
function makeDelayedText() {
let _resolve, _reject;
let promise = new Promise((resolve, reject) => {
_resolve = () => {
promise = null;
resolve();
};
_reject = e => {
promise = null;
reject(e);
};
});
async function DelayedText({children}) {
await promise;
return <Text>{children}</Text>;
}
return [DelayedText, _resolve, _reject];
}
const [Friends, resolveFriends] = makeDelayedText();
const [Name, resolveName] = makeDelayedText();
const [Posts, resolvePosts] = makeDelayedText();
const [Photos, resolvePhotos] = makeDelayedText();
const [Games, , rejectGames] = makeDelayedText();
// View
function ProfileDetails({avatar}) {
return (
<div>
<Name>:name:</Name>
{avatar}
</div>
);
}
function ProfileSidebar({friends}) {
return (
<div>
<Photos>:photos:</Photos>
{friends}
</div>
);
}
function ProfilePosts({posts}) {
return <div>{posts}</div>;
}
function ProfileGames({games}) {
return <div>{games}</div>;
}
const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
function ProfileContent() {
return (
<>
<ProfileDetails avatar={<Text>:avatar:</Text>} />
<Suspense fallback={<p>(loading sidebar)</p>}>
<ProfileSidebar friends={<Friends>:friends:</Friends>} />
</Suspense>
<Suspense fallback={<p>(loading posts)</p>}>
<ProfilePosts posts={<Posts>:posts:</Posts>} />
</Suspense>
<MyErrorBoundaryClient>
<Suspense fallback={<p>(loading games)</p>}>
<ProfileGames games={<Games>:games:</Games>} />
</Suspense>
</MyErrorBoundaryClient>
</>
);
}
const model = {
rootContent: <ProfileContent />,
};
function ProfilePage({response}) {
return use(response).rootContent;
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(model, webpackMap, {
onError(x) {
reportedErrors.push(x);
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
}),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<ProfilePage response={response} />
</Suspense>,
);
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
// This isn't enough to show anything.
await serverAct(async () => {
await act(() => {
resolveFriends();
});
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
// We can now show the details. Sidebar and posts are still loading.
await serverAct(async () => {
await act(() => {
resolveName();
});
});
// Advance time enough to trigger a nested fallback.
await act(() => {
jest.advanceTimersByTime(500);
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
'<p>(loading posts)</p>' +
'<p>(loading games)</p>',
);
expect(reportedErrors).toEqual([]);
const theError = new Error('Game over');
// Let's *fail* loading games.
await serverAct(async () => {
await act(async () => {
await rejectGames(theError);
await 'the inner async function';
});
});
const expectedGamesValue = __DEV__
? '<p>Game over + a dev digest</p>'
: '<p>digest("Game over")</p>';
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
'<p>(loading posts)</p>' +
expectedGamesValue,
);
expect(reportedErrors).toEqual([theError]);
reportedErrors = [];
// We can now show the sidebar.
await serverAct(async () => {
await act(async () => {
await resolvePhotos();
await 'the inner async function';
});
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<p>(loading posts)</p>' +
expectedGamesValue,
);
// Show everything.
await serverAct(async () => {
await act(async () => {
await resolvePosts();
await 'the inner async function';
});
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<div>:posts:</div>' +
expectedGamesValue,
);
expect(reportedErrors).toEqual([]);
});
it('should handle streaming async server components', async () => {
const reportedErrors = [];
const Row = async ({current, next}) => {
const chunk = await next;
if (chunk.done) {
return chunk.value;
}
return (
<Suspense fallback={chunk.value}>
<Row current={chunk.value} next={chunk.next} />
</Suspense>
);
};
function createResolvablePromise() {
let _resolve, _reject;
const promise = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
return {promise, resolve: _resolve, reject: _reject};
}
function createSuspendedChunk(initialValue) {
const {promise, resolve, reject} = createResolvablePromise();
return {
row: (
<Suspense fallback={initialValue}>
<Row current={initialValue} next={promise} />
</Suspense>
),
resolve,
reject,
};
}
function makeDelayedText() {
const {promise, resolve, reject} = createResolvablePromise();
async function DelayedText() {
const data = await promise;
return <div>{data}</div>;
}
return [DelayedText, resolve, reject];
}
const [Posts, resolvePostsData] = makeDelayedText();
const [Photos, resolvePhotosData] = makeDelayedText();
const suspendedChunk = createSuspendedChunk(<p>loading</p>);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
suspendedChunk.row,
webpackMap,
{
onError(error) {
reportedErrors.push(error);
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function ClientRoot() {
return use(response);
}
await act(() => {
root.render(<ClientRoot />);
});
expect(container.innerHTML).toBe('<p>loading</p>');
const donePromise = createResolvablePromise();
const value = (
<Suspense fallback={<p>loading posts and photos</p>}>
<Posts />
<Photos />
</Suspense>
);
await serverAct(async () => {
await act(async () => {
suspendedChunk.resolve({value, done: false, next: donePromise.promise});
donePromise.resolve({value, done: true});
});
});
expect(container.innerHTML).toBe('<p>loading posts and photos</p>');
await serverAct(async () => {
await act(async () => {
await resolvePostsData('posts');
await resolvePhotosData('photos');
});
});
expect(container.innerHTML).toBe('<div>posts</div><div>photos</div>');
expect(reportedErrors).toEqual([]);
});
it('should preserve state of client components on refetch', async () => {
// Client
function Page({response}) {
return use(response);
}
function Input() {
return <input />;
}
const InputClient = clientExports(Input);
// Server
function App({color}) {
// Verify both DOM and Client children.
return (
<div style={{color}}>
<input />
<InputClient />
</div>
);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const stream1 = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<App color="red" />,
webpackMap,
),
);
pipe(stream1.writable);
const response1 = ReactServerDOMClient.createFromReadableStream(
stream1.readable,
);
await act(() => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<Page response={response1} />
</Suspense>,
);
});
expect(container.children.length).toBe(1);
expect(container.children[0].tagName).toBe('DIV');
expect(container.children[0].style.color).toBe('red');
// Change the DOM state for both inputs.
const inputA = container.children[0].children[0];
expect(inputA.tagName).toBe('INPUT');
inputA.value = 'hello';
const inputB = container.children[0].children[1];
expect(inputB.tagName).toBe('INPUT');
inputB.value = 'goodbye';
const stream2 = getTestStream();
const {pipe: pipe2} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<App color="blue" />,
webpackMap,
),
);
pipe2(stream2.writable);
const response2 = ReactServerDOMClient.createFromReadableStream(
stream2.readable,
);
await act(() => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<Page response={response2} />
</Suspense>,
);
});
expect(container.children.length).toBe(1);
expect(container.children[0].tagName).toBe('DIV');
expect(container.children[0].style.color).toBe('blue');
// Verify we didn't destroy the DOM for either input.
expect(inputA === container.children[0].children[0]).toBe(true);
expect(inputA.tagName).toBe('INPUT');
expect(inputA.value).toBe('hello');
expect(inputB === container.children[0].children[1]).toBe(true);
expect(inputB.tagName).toBe('INPUT');
expect(inputB.value).toBe('goodbye');
});
it('should be able to complete after aborting and throw the reason client-side', async () => {
const reportedErrors = [];
const {writable, readable} = getTestStream();
const {pipe, abort} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<div>
<InfiniteSuspend />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
const message = typeof x === 'string' ? x : x.message;
return __DEV__ ? 'a dev digest' : `digest("${message}")`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App({res}) {
return use(res);
}
await act(() => {
root.render(
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await act(() => {
abort('for reasons');
});
if (__DEV__) {
expect(container.innerHTML).toBe('<p>for reasons + a dev digest</p>');
} else {
expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
}
expect(reportedErrors).toEqual(['for reasons']);
});
it('should be able to recover from a direct reference erroring client-side', async () => {
const reportedErrors = [];
const ClientComponent = clientExports(function ({prop}) {
return 'This should never render';
});
const ClientReference = clientModuleError(new Error('module init error'));
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<div>
<ClientComponent prop={ClientReference} />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App({res}) {
return use(res);
}
await act(() => {
root.render(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>module init error</p>');
expect(reportedErrors).toEqual([]);
});
it('should be able to recover from a direct reference erroring client-side async', async () => {
const reportedErrors = [];
const ClientComponent = clientExports(function ({prop}) {
return 'This should never render';
});
let rejectPromise;
const ClientReference = await clientExports(
new Promise((resolve, reject) => {
rejectPromise = reject;
}),
);
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<div>
<ClientComponent prop={ClientReference} />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App({res}) {
return use(res);
}
await act(() => {
root.render(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await act(() => {
rejectPromise(new Error('async module init error'));
});
expect(container.innerHTML).toBe('<p>async module init error</p>');
expect(reportedErrors).toEqual([]);
});
it('should be able to recover from a direct reference erroring server-side', async () => {
const reportedErrors = [];
const ClientComponent = clientExports(function ({prop}) {
return 'This should never render';
});
// We simulate a bug in the Webpack bundler which causes an error on the server.
for (const id in webpackMap) {
Object.defineProperty(webpackMap, id, {
get: () => {
throw new Error('bug in the bundler');
},
});
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<div>
<ClientComponent />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x.message);
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App({res}) {
return use(res);
}
await act(() => {
root.render(
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
if (__DEV__) {
expect(container.innerHTML).toBe(
'<p>bug in the bundler + a dev digest</p>',
);
} else {
expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
}
expect(reportedErrors).toEqual(['bug in the bundler']);
});
it('should pass a Promise through props and be able use() it on the client', async () => {
async function getData() {
return 'async hello';
}
function Component({data}) {
const text = use(data);
return <p>{text}</p>;
}
const ClientComponent = clientExports(Component);
function ServerComponent() {
const data = getData(); // no await here
return <ClientComponent data={data} />;
}
function Print({response}) {
return use(response);
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>async hello</p>');
});
it('should throw on the client if a passed promise eventually rejects', async () => {
const reportedErrors = [];
const theError = new Error('Server throw');
async function getData() {
throw theError;
}
function Component({data}) {
const text = use(data);
return <p>{text}</p>;
}
const ClientComponent = clientExports(Component);
function ServerComponent() {
const data = getData(); // no await here
return <ClientComponent data={data} />;
}
function Await({response}) {
return use(response);
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
<Await response={response} />
</ErrorBoundary>
</Suspense>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe(
__DEV__
? '<p>Server throw + a dev digest</p>'
: '<p>digest("Server throw")</p>',
);
expect(reportedErrors).toEqual([theError]);
});
it('should support float methods when rendering in Fiber', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
FlightReactDOM.prefetchDNS('d before');
FlightReactDOM.preconnect('c before');
FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('l before', {as: 'style'});
FlightReactDOM.preloadModule('lm before');
FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
FlightReactDOM.preinit('i before', {as: 'script'});
FlightReactDOM.preinitModule('m before');
FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
await 1;
FlightReactDOM.prefetchDNS('d after');
FlightReactDOM.preconnect('c after');
FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('l after', {as: 'style'});
FlightReactDOM.preloadModule('lm after');
FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
FlightReactDOM.preinit('i after', {as: 'script'});
FlightReactDOM.preinitModule('m after');
FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
return <ClientComponent />;
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
),
);
pipe(writable);
let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(readable);
}
return response;
}
function App() {
return getResponse();
}
// We pause to allow the float call after the await point to process before the
// HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
// because the module graphs are not different and the HostDispatcher is shared.
// In a real environment the Fiber and Flight code would each have their own independent
// dispatcher.
// @TODO consider what happens when Server-Components-On-The-Client exist. we probably
// want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
// but we need to make sure that actually makes sense
await 1;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="dns-prefetch" href="d before" />
<link rel="preconnect" href="c before" />
<link rel="preconnect" href="c2 before" crossorigin="" />
<link rel="preload" as="style" href="l before" />
<link rel="modulepreload" href="lm before" />
<link rel="modulepreload" href="lm2 before" crossorigin="" />
<script async="" src="i before" />
<script type="module" async="" src="m before" />
<script type="module" async="" src="m2 before" crossorigin="" />
<link rel="dns-prefetch" href="d after" />
<link rel="preconnect" href="c after" />
<link rel="preconnect" href="c2 after" crossorigin="" />
<link rel="preload" as="style" href="l after" />
<link rel="modulepreload" href="lm after" />
<link rel="modulepreload" href="lm2 after" crossorigin="" />
<script async="" src="i after" />
<script type="module" async="" src="m after" />
<script type="module" async="" src="m2 after" crossorigin="" />
</head>
<body />
</html>,
);
expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
});
// @gate enablePostpone
it('should allow postponing in Flight through a serialized promise', async () => {
const Context = React.createContext();
const ContextProvider = Context.Provider;
function Foo() {
const value = React.use(React.useContext(Context));
return <span>{value}</span>;
}
const ClientModule = clientExports({
ContextProvider,
Foo,
});
async function getFoo() {
React.unstable_postpone('foo');
}
function App() {
return (
<ClientModule.ContextProvider value={getFoo()}>
<div>
<Suspense fallback="loading...">
<ClientModule.Foo />
</Suspense>
</div>
</ClientModule.ContextProvider>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
);
pipe(writable);
let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(readable);
}
return response;
}
function Response() {
return getResponse();
}
const errors = [];
function onError(error, errorInfo) {
errors.push(error, errorInfo);
}
const result = await serverAct(() =>
ReactDOMStaticServer.prerenderToNodeStream(<Response />, {
onError,
}),
);
const prelude = await new Promise((resolve, reject) => {
let content = '';
result.prelude.on('data', chunk => {
content += Buffer.from(chunk).toString('utf8');
});
result.prelude.on('error', error => {
reject(error);
});
result.prelude.on('end', () => resolve(content));
});
expect(errors).toEqual([]);
const doc = new JSDOM(prelude).window.document;
expect(getMeaningfulChildren(doc)).toEqual(
<html>
<head />
<body>
<div>loading...</div>
</body>
</html>,
);
});
it('should support float methods when rendering in Fizz', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
FlightReactDOM.prefetchDNS('d before');
FlightReactDOM.preconnect('c before');
FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('l before', {as: 'style'});
FlightReactDOM.preloadModule('lm before');
FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
FlightReactDOM.preinit('i before', {as: 'script'});
FlightReactDOM.preinitModule('m before');
FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
await 1;
FlightReactDOM.prefetchDNS('d after');
FlightReactDOM.preconnect('c after');
FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('l after', {as: 'style'});
FlightReactDOM.preloadModule('lm after');
FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
FlightReactDOM.preinit('i after', {as: 'script'});
FlightReactDOM.preinitModule('m after');
FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
return <ClientComponent />;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
// In a real environment you would want to call the render during the Fizz render.
// The reason we cannot do this in our test is because we don't actually have two separate
// module graphs and we are contriving the sequencing to work in a way where
// the right HostDispatcher is in scope during the Flight Server Float calls and the
// Flight Client hint dispatches
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
),
);
pipe(flightWritable);
let response = null;
function getResponse() {
if (response === null) {
response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
}
return response;
}
function App() {
return (
<html>
<body>{getResponse()}</body>
</html>
);
}
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
});
await readInto(document, fizzReadable);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="dns-prefetch" href="d before" />
<link rel="preconnect" href="c before" />
<link rel="preconnect" href="c2 before" crossorigin="" />
<link rel="dns-prefetch" href="d after" />
<link rel="preconnect" href="c after" />
<link rel="preconnect" href="c2 after" crossorigin="" />
<script async="" src="i before" />
<script type="module" async="" src="m before" />
<script type="module" async="" src="m2 before" crossorigin="" />
<script async="" src="i after" />
<script type="module" async="" src="m after" />
<script type="module" async="" src="m2 after" crossorigin="" />
<link rel="preload" as="style" href="l before" />
<link rel="modulepreload" href="lm before" />
<link rel="modulepreload" href="lm2 before" crossorigin="" />
<link rel="preload" as="style" href="l after" />
<link rel="modulepreload" href="lm after" />
<link rel="modulepreload" href="lm2 after" crossorigin="" />
</head>
<body>
<p>hello world</p>
</body>
</html>,
);
});
it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent1() {
FlightReactDOM.preload('before1', {as: 'style'});
await 1;
FlightReactDOM.preload('after1', {as: 'style'});
return <ClientComponent />;
}
async function ServerComponent2() {
FlightReactDOM.preload('before2', {as: 'style'});
await 1;
FlightReactDOM.preload('after2', {as: 'style'});
return <ClientComponent />;
}
const {writable: flightWritable1, readable: flightReadable1} =
getTestStream();
const {writable: flightWritable2, readable: flightReadable2} =
getTestStream();
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent1 />,
webpackMap,
).pipe(flightWritable1);
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent2 />,
webpackMap,
).pipe(flightWritable2);
const responses = new Map();
function getResponse(stream) {
let response = responses.get(stream);
if (!response) {
response = ReactServerDOMClient.createFromReadableStream(stream);
responses.set(stream, response);
}
return response;
}
function App({stream}) {
return (
<html>
<body>{getResponse(stream)}</body>
</html>
);
}
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
// we aren't operating separate module graphs for flight and fiber. In a real app
// each would have their own dispatcher and there would be no cross dispatching.
await serverAct(() => {});
const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
<App stream={flightReadable1} />,
).pipe(fizzWritable1);
ReactDOMFizzServer.renderToPipeableStream(
<App stream={flightReadable2} />,
).pipe(fizzWritable2);
});
async function read(stream) {
const decoder = new TextDecoder();
const reader = stream.getReader();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
buffer += decoder.decode();
break;
}
buffer += decoder.decode(value, {stream: true});
}
return buffer;
}
const [content1, content2] = await Promise.all([
read(fizzReadable1),
read(fizzReadable2),
]);
expect(content1).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
'<link rel="preload" href="after1" as="style"/>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#«R»" blocking="render"/>'
: '') +
'</head>' +
'<body><p>hello world</p>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="«R»"></template>'
: '') +
'</body></html>',
);
expect(content2).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
'<link rel="preload" href="after2" as="style"/>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<link rel="expect" href="#«R»" blocking="render"/>'
: '') +
'</head>' +
'<body><p>hello world</p>' +
(gate(flags => flags.enableFizzBlockingRender)
? '<template id="«R»"></template>'
: '') +
'</body></html>',
);
});
it('supports deduping hints by Float key', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
FlightReactDOM.prefetchDNS('dns');
FlightReactDOM.preconnect('preconnect');
FlightReactDOM.preload('load', {as: 'style'});
FlightReactDOM.preinit('init', {as: 'script'});
// again but vary preconnect to demonstrate crossOrigin participates in the key
FlightReactDOM.prefetchDNS('dns');
FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('load', {as: 'style'});
FlightReactDOM.preinit('init', {as: 'script'});
await 1;
// after an async point
FlightReactDOM.prefetchDNS('dns');
FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
FlightReactDOM.preload('load', {as: 'style'});
FlightReactDOM.preinit('init', {as: 'script'});
return <ClientComponent />;
}
const {writable, readable} = getTestStream();
await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
).pipe(writable),
);
const hintRows = [];
async function collectHints(stream) {
const decoder = new TextDecoder();
const reader = stream.getReader();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
buffer += decoder.decode();
if (buffer.includes(':H')) {
hintRows.push(buffer);
}
break;
}
buffer += decoder.decode(value, {stream: true});
let line;
while ((line = buffer.indexOf('\n')) > -1) {
const row = buffer.slice(0, line);
buffer = buffer.slice(line + 1);
if (row.includes(':H')) {
hintRows.push(row);
}
}
}
}
await collectHints(readable);
expect(hintRows.length).toEqual(6);
});
it('should be able to include a client reference in printed errors', async () => {
const reportedErrors = [];
const ClientComponent = clientExports(function ({prop}) {
return 'This should never render';
});
const ClientReference = clientExports({});
class InvalidValue {}
const {writable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<div>
<ClientComponent prop={ClientReference} invalid={InvalidValue} />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
),
);
pipe(writable);
expect(reportedErrors.length).toBe(1);
if (__DEV__) {
expect(reportedErrors[0].message).toEqual(
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
'Or maybe you meant to call this function rather than return it.\n' +
' <... prop={client} invalid={function InvalidValue}>\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^',
);
} else {
expect(reportedErrors[0].message).toEqual(
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
'Or maybe you meant to call this function rather than return it.\n' +
' {prop: client, invalid: function InvalidValue}\n' +
' ^^^^^^^^^^^^^^^^^^^^^',
);
}
});
it('should be able to render a client reference as return value', async () => {
const ClientModule = clientExports({
text: 'Hello World',
});
function ServerComponent() {
return ClientModule.text;
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<ServerComponent />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(response);
});
expect(container.innerHTML).toBe('Hello World');
});
it('can abort synchronously during render', async () => {
function Sibling() {
return <p>sibling</p>;
}
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>
<ComponentThatAborts />
<Sibling />
</Suspense>
<Suspense fallback={<p>loading 2...</p>}>
<Sibling />
</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>
<div>
<Sibling />
</div>
</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
function ComponentThatAborts() {
abortRef.current();
return <p>hello world</p>;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>
<p>loading 3...</p>
</div>
</div>,
);
});
it('can abort during render in an async tick', async () => {
async function Sibling() {
return <p>sibling</p>;
}
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>
<ComponentThatAborts />
<Sibling />
</Suspense>
<Suspense fallback={<p>loading 2...</p>}>
<Sibling />
</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>
<div>
<Sibling />
</div>
</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
async function ComponentThatAborts() {
await 1;
abortRef.current();
return <p>hello world</p>;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>
<p>loading 3...</p>
</div>
</div>,
);
});
it('can abort during render in a lazy initializer for a component', async () => {
function Sibling() {
return <p>sibling</p>;
}
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>
<LazyAbort />
</Suspense>
<Suspense fallback={<p>loading 2...</p>}>
<Sibling />
</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>
<div>
<Sibling />
</div>
</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
const LazyAbort = React.lazy(() => {
abortRef.current();
return {
then(cb) {
cb({default: 'div'});
},
};
});
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>
<p>loading 3...</p>
</div>
</div>,
);
});
it('can abort during render in a lazy initializer for an element', async () => {
function Sibling() {
return <p>sibling</p>;
}
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>{lazyAbort}</Suspense>
<Suspense fallback={<p>loading 2...</p>}>
<Sibling />
</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>
<div>
<Sibling />
</div>
</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
const lazyAbort = React.lazy(() => {
abortRef.current();
return {
then(cb) {
cb({default: 'hello world'});
},
};
});
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>
<p>loading 3...</p>
</div>
</div>,
);
});
it('can abort during a synchronous thenable resolution', async () => {
function Sibling() {
return <p>sibling</p>;
}
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>{thenable}</Suspense>
<Suspense fallback={<p>loading 2...</p>}>
<Sibling />
</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>
<div>
<Sibling />
</div>
</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
const thenable = {
then(cb) {
abortRef.current();
cb(thenable.value);
},
};
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>
<p>loading 3...</p>
</div>
</div>,
);
});
it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
function App() {
return (
<div>
<Suspense fallback={<p>loading 1...</p>}>
<ComponentThatAborts />
</Suspense>
<Suspense fallback={<p>loading 2...</p>}>{thenable1}</Suspense>
<div>
<Suspense fallback={<p>loading 3...</p>}>{thenable2}</Suspense>
</div>
</div>
);
}
const abortRef = {current: null};
const thenable1 = {
then(cb) {
cb('hello world');
},
};
const thenable2 = {
then(cb) {
cb('hello world');
},
status: 'fulfilled',
value: 'hello world',
};
function ComponentThatAborts() {
abortRef.current();
return thenable1;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading 1...</p>
<p>loading 2...</p>
<div>hello world</div>
</div>,
);
});
it('can error synchronously after aborting without an unhandled rejection error', async () => {
function App() {
return (
<div>
<Suspense fallback={<p>loading...</p>}>
<ComponentThatAborts />
</Suspense>
</div>
);
}
const abortRef = {current: null};
async function ComponentThatAborts() {
abortRef.current();
throw new Error('boom');
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading...</p>
</div>,
);
});
it('can error synchronously after aborting in a synchronous Component', async () => {
const rejectError = new Error('bam!');
const rejectedPromise = Promise.reject(rejectError);
rejectedPromise.catch(() => {});
rejectedPromise.status = 'rejected';
rejectedPromise.reason = rejectError;
const resolvedValue = <p>hello world</p>;
const resolvedPromise = Promise.resolve(resolvedValue);
resolvedPromise.status = 'fulfilled';
resolvedPromise.value = resolvedValue;
function App() {
return (
<div>
<Suspense fallback={<p>loading...</p>}>
<ComponentThatAborts />
</Suspense>
<Suspense fallback={<p>loading too...</p>}>
{rejectedPromise}
</Suspense>
<Suspense fallback={<p>loading three...</p>}>
{resolvedPromise}
</Suspense>
</div>
);
}
const abortRef = {current: null};
// This test is specifically asserting that this works with Sync Server Component
function ComponentThatAborts() {
abortRef.current();
throw new Error('boom');
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
{
onError(e) {
console.error(e);
},
},
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'bam!',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'bam!',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading...</p>
<p>loading too...</p>
<p>hello world</p>
</div>,
);
});
// @gate experimental
it('can prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
});
function App() {
return (
<div>
<Greeting />
</div>
);
}
async function Greeting() {
await greetingPromise;
return 'hello world';
}
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
pendingResult:
ReactServerDOMStaticServer.unstable_prerenderToNodeStream(
<App />,
webpackMap,
),
};
});
resolveGreeting();
const {prelude} = await pendingResult;
const response = ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
});
// @gate enableHalt
it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
});
function App() {
return (
<div>
<Suspense fallback="loading...">
<Greeting />
</Suspense>
</div>
);
}
async function Greeting() {
await greetingPromise;
return 'hello world';
}
const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
// destructure trick to avoid the act scope from awaiting the returned value
return {
pendingResult:
ReactServerDOMStaticServer.unstable_prerenderToNodeStream(
<App />,
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});
controller.abort('boom');
resolveGreeting();
const {prelude} = await pendingResult;
expect(errors).toEqual([]);
const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onError(error) {
errors.push(error);
},
},
);
pipe(fizzWritable);
abortFizz = abort;
});
await serverAct(() => {
abortFizz('bam');
});
expect(errors).toEqual([new Error('Connection closed.')]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
});
// @gate enableHalt
it('will leave async iterables in an incomplete state when halting', async () => {
let resolve;
const wait = new Promise(r => (resolve = r));
const errors = [];
const multiShotIterable = {
async *[Symbol.asyncIterator]() {
yield {hello: 'A'};
await wait;
yield {hi: 'B'};
return 'C';
},
};
const controller = new AbortController();
const {pendingResult} = await serverAct(() => {
return {
pendingResult:
ReactServerDOMStaticServer.unstable_prerenderToNodeStream(
{
multiShotIterable,
},
{},
{
onError(x) {
errors.push(x);
},
signal: controller.signal,
},
),
};
});
controller.abort();
await serverAct(() => resolve());
const {prelude} = await pendingResult;
const result = await ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
);
const iterator = result.multiShotIterable[Symbol.asyncIterator]();
expect(await iterator.next()).toEqual({
value: {hello: 'A'},
done: false,
});
const race = Promise.race([
iterator.next(),
new Promise(r => setTimeout(() => r('timeout'), 10)),
]);
await 1;
jest.advanceTimersByTime('100');
expect(await race).toBe('timeout');
});
// @gate enableHalt
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
const controller = new AbortController();
function ComponentThatAborts() {
controller.abort('boom');
return null;
}
async function Greeting() {
await 1;
return 'hello world';
}
async function Farewell() {
return 'goodbye world';
}
async function Wrapper() {
return (
<Suspense fallback="loading too...">
<ComponentThatAborts />
</Suspense>
);
}
function App() {
return (
<div>
<Suspense fallback="loading...">
<Greeting />
</Suspense>
<Wrapper />
<Suspense fallback="loading three...">
<Farewell />
</Suspense>
</div>
);
}
const errors = [];
const {pendingResult} = await serverAct(() => {
return {
pendingResult:
ReactServerDOMStaticServer.unstable_prerenderToNodeStream(
<App />,
{},
{
onError(x) {
errors.push(x);
},
signal: controller.signal,
},
),
};
});
const {prelude} = await pendingResult;
expect(errors).toEqual([]);
const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onError(error, errorInfo) {
errors.push(error);
},
},
);
pipe(fizzWritable);
abortFizz = abort;
});
await serverAct(() => {
abortFizz('boom');
});
// one error per boundary
const err = new Error('Connection closed.');
expect(errors).toEqual([err, err, err]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
{'loading...'}
{'loading too...'}
{'loading three...'}
</div>,
);
});
it('rejecting a thenable after an abort before flush should not lead to a frozen readable', async () => {
const ClientComponent = clientExports(function (props: {
promise: Promise<void>,
}) {
return 'hello world';
});
let reject;
const promise = new Promise((_, re) => {
reject = re;
});
function App() {
return (
<div>
<Suspense fallback="loading...">
<ClientComponent promise={promise} />
</Suspense>
</div>
);
}
const errors = [];
const {writable, readable} = getTestStream();
const {pipe, abort} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap, {
onError(x) {
errors.push(x);
},
}),
);
await serverAct(() => {
abort('STOP');
reject('STOP');
});
pipe(writable);
const reader = readable.getReader();
while (true) {
const {done} = await reader.read();
if (done) {
break;
}
}
expect(errors).toEqual(['STOP']);
// We expect it to get to the end here rather than hang on the reader.
});
});