'use strict';
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
import {Readable} from 'stream';
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(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
ReactServerScheduler = require('scheduler');
patchSetImmediate(ReactServerScheduler);
reactServerAct = require('internal-test-utils').act;
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');
}
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 ) {
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 (
node.hasAttribute('data-meaningful') ||
(node.tagName === 'SCRIPT' &&
node.hasAttribute('src') &&
node.hasAttribute('async')) ||
(node.tagName !== 'SCRIPT' &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
!node.hasAttribute('aria-hidden'))
) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
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 () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}
function RootModel() {
return {
html: <HTML />,
};
}
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 () => {
function RootModel() {
return {text: '$1'};
}
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 () => {
function RootModel() {
return {text: '@div'};
}
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) {
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 = {
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 = [];
function MyErrorBoundary({children}) {
return (
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
{children}
</ErrorBoundary>
);
}
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();
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>');
await serverAct(async () => {
await act(() => {
resolveFriends();
});
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await serverAct(async () => {
await act(() => {
resolveName();
});
});
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');
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 = [];
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,
);
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 () => {
function Page({response}) {
return use(response);
}
function Input() {
return <input />;
}
const InputClient = clientExports(Input);
function App({color}) {
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');
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');
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';
});
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();
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();
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();
}
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>);
});
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();
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>
);
}
await serverAct(() => {});
const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
<App stream={flightReadable1} />,
).pipe(fizzWritable1);
ReactDOMFizzServer.renderToPipeableStream(
<App stream={flightReadable2} />,
).pipe(fizzWritable2);
});
async function read(stream) {
const decoder = new TextDecoder();
const reader = stream.getReader();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
buffer += decoder.decode();
break;
}
buffer += decoder.decode(value, {stream: true});
}
return buffer;
}
const [content1, content2] = await Promise.all([
read(fizzReadable1),
read(fizzReadable2),
]);
expect(content1).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
'<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
);
expect(content2).toEqual(
'<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
'<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
);
});
it('supports deduping hints by Float key', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
FlightReactDOM.prefetchDNS('dns');
FlightReactDOM.preconnect('preconnect');
FlightReactDOM.preload('load', {as: 'style'});
FlightReactDOM.preinit('init', {as: 'script'});
FlightReactDOM.prefetchDNS('dns');
FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
FlightReactDOM.preload('load', {as: 'style'});
FlightReactDOM.preinit('init', {as: 'script'});
await 1;
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};
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>,
);
});
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 () => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
webpackMap,
),
};
});
resolveGreeting();
const {prelude} = await pendingResult;
const response = ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
});
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 () => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});
controller.abort('boom');
resolveGreeting();
const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onError(error) {
errors.push(error);
},
},
);
pipe(fizzWritable);
abortFizz = abort;
});
await serverAct(() => {
abortFizz('bam');
});
if (__DEV__) {
expect(errors).toEqual([new Error('Connection closed.')]);
} else {
expect(errors).toEqual(['bam']);
}
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
});
it('will leave async iterables in an incomplete state when halting', async () => {
let resolve;
const wait = new Promise(r => (resolve = r));
const errors = [];
const multiShotIterable = {
async *[Symbol.asyncIterator]() {
yield {hello: 'A'};
await wait;
yield {hi: 'B'};
return 'C';
},
};
const controller = new AbortController();
const {pendingResult} = await serverAct(() => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
{
multiShotIterable,
},
{},
{
onError(x) {
errors.push(x);
},
signal: controller.signal,
},
),
};
});
controller.abort();
await serverAct(() => resolve());
const {prelude} = await pendingResult;
const result = await ReactServerDOMClient.createFromReadableStream(
Readable.toWeb(prelude),
);
const iterator = result.multiShotIterable[Symbol.asyncIterator]();
expect(await iterator.next()).toEqual({
value: {hello: 'A'},
done: false,
});
const race = Promise.race([
iterator.next(),
new Promise(r => setTimeout(() => r('timeout'), 10)),
]);
await 1;
jest.advanceTimersByTime('100');
expect(await race).toBe('timeout');
});
it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
const controller = new AbortController();
function ComponentThatAborts() {
controller.abort('boom');
return null;
}
async function Greeting() {
await 1;
return 'hello world';
}
async function Farewell() {
return 'goodbye world';
}
async function Wrapper() {
return (
<Suspense fallback="loading too...">
<ComponentThatAborts />
</Suspense>
);
}
function App() {
return (
<div>
<Suspense fallback="loading...">
<Greeting />
</Suspense>
<Wrapper />
<Suspense fallback="loading three...">
<Farewell />
</Suspense>
</div>
);
}
const errors = [];
const {pendingResult} = await serverAct(() => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
{},
{
onError(x) {
errors.push(x);
},
signal: controller.signal,
},
),
};
});
const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
const preludeWeb = Readable.toWeb(prelude);
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
errors.length = 0;
let abortFizz;
await serverAct(async () => {
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onError(error, errorInfo) {
errors.push(error);
},
},
);
pipe(fizzWritable);
abortFizz = abort;
});
await serverAct(() => {
abortFizz('boom');
});
if (__DEV__) {
const err = new Error('Connection closed.');
expect(errors).toEqual([err, err, err]);
} else {
expect(errors).toEqual(['boom', 'boom', 'boom']);
}
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
{'loading...'}
{'loading too...'}
{'loading three...'}
</div>,
);
});
});