'use strict';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
const {
patchMessageChannel,
} = require('../../../../scripts/jest/patchMessageChannel');
let clientExports;
let serverExports;
let webpackMap;
let webpackServerMap;
let act;
let serverAct;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let Suspense;
let use;
let ReactServer;
let ReactServerDOM;
let ReactServerScheduler;
let assertConsoleErrorDev;
describe('ReactFlightDOMBrowser', () => {
beforeEach(() => {
jest.resetModules();
ReactServerScheduler = require('scheduler');
patchMessageChannel(ReactServerScheduler);
serverAct = require('internal-test-utils').serverAct;
jest.mock('react', () => require('react/react.react-server'));
ReactServer = require('react');
ReactServerDOM = require('react-dom');
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.browser'),
);
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
serverExports = WebpackMock.serverExports;
webpackMap = WebpackMock.webpackMap;
webpackServerMap = WebpackMock.webpackServerMap;
ReactServerDOMServer = require('react-server-dom-webpack/server');
if (__EXPERIMENTAL__) {
jest.mock('react-server-dom-webpack/static', () =>
require('react-server-dom-webpack/static.browser'),
);
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
}
__unmockReact();
jest.resetModules();
patchMessageChannel();
({act, assertConsoleErrorDev} = require('internal-test-utils'));
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server.browser');
ReactServerDOMClient = require('react-server-dom-webpack/client');
Suspense = React.Suspense;
use = React.use;
});
function makeDelayedText(Model) {
let error, _resolve, _reject;
let promise = new Promise((resolve, reject) => {
_resolve = () => {
promise = null;
resolve();
};
_reject = e => {
error = e;
promise = null;
reject(e);
};
});
function DelayedText({children}, data) {
if (promise) {
throw promise;
}
if (error) {
throw error;
}
return <Model>{children}</Model>;
}
return [DelayedText, _resolve, _reject];
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}
function requireServerRef(ref) {
let name = '';
let resolvedModuleData = webpackServerMap[ref];
if (resolvedModuleData) {
name = resolvedModuleData.name;
} else {
const idx = ref.lastIndexOf('#');
if (idx !== -1) {
name = ref.slice(idx + 1);
resolvedModuleData = webpackServerMap[ref.slice(0, idx)];
}
if (!resolvedModuleData) {
throw new Error(
'Could not find the module "' +
ref +
'" in the React Client Manifest. ' +
'This is probably a bug in the React Server Components bundler.',
);
}
}
const mod = __webpack_require__(resolvedModuleData.id);
if (name === '*') {
return mod;
}
return mod[name];
}
async function callServer(actionId, body) {
const fn = requireServerRef(actionId);
const args = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
return fn.apply(null, args);
}
it('should resolve HTML using W3C streams', async () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}
function App() {
const model = {
html: <HTML />,
};
return model;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<App />),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
const model = await response;
expect(model).toEqual({
html: (
<div>
<span>hello</span>
<span>world</span>
</div>
),
});
});
it('should resolve client components (with async chunks) when referenced in props', async () => {
let resolveClientComponentChunk;
const ClientOuter = clientExports(function ClientOuter({
Component,
children,
}) {
return <Component>{children}</Component>;
});
const ClientInner = clientExports(
function ClientInner({children}) {
return <span>{children}</span>;
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function Server() {
return <ClientOuter Component={ClientInner}>Hello, World!</ClientOuter>;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveClientComponentChunk();
});
expect(container.innerHTML).toBe('<span>Hello, World!</span>');
});
it('should resolve deduped objects within the same model root when it is blocked', async () => {
let resolveClientComponentChunk;
const ClientOuter = clientExports(function ClientOuter({Component, value}) {
return <Component value={value} />;
});
const ClientInner = clientExports(
function ClientInner({value}) {
return <pre>{JSON.stringify(value)}</pre>;
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function Server({value}) {
return <ClientOuter Component={ClientInner} value={value} />;
}
const shared = [1, 2, 3];
const value = [shared, shared];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<Server value={value} />,
webpackMap,
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveClientComponentChunk();
});
expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
});
it('should resolve deduped objects within the same model root when it is blocked and there is a listener attached to the root', async () => {
let resolveClientComponentChunk;
const ClientOuter = clientExports(function ClientOuter({Component, value}) {
return <Component value={value} />;
});
const ClientInner = clientExports(
function ClientInner({value}) {
return <pre>{JSON.stringify(value)}</pre>;
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function Server({value}) {
return <ClientOuter Component={ClientInner} value={value} />;
}
const shared = [1, 2, 3];
const value = [shared, shared];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<Server value={value} />,
webpackMap,
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
response.then(() => {});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveClientComponentChunk();
});
expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
});
it('should resolve deduped objects that are themselves blocked', async () => {
let resolveClientComponentChunk;
const Client = clientExports(
[4, 5],
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
const shared = [1, 2, 3, Client];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<div>
<Suspense fallback="Loading">
<span>
{shared /* this will serialize first and block nearest element */}
</span>
</Suspense>
{shared /* this will be referenced inside the blocked element */}
</div>,
webpackMap,
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveClientComponentChunk();
});
expect(container.innerHTML).toBe('<div><span>12345</span>12345</div>');
});
it('should resolve deduped objects in nested children of blocked models', async () => {
let resolveOuterClientComponentChunk;
let resolveInnerClientComponentChunk;
const ClientOuter = clientExports(
function ClientOuter({children, value}) {
return children;
},
'1',
'/outer.js',
new Promise(resolve => (resolveOuterClientComponentChunk = resolve)),
);
function PassthroughServerComponent({children}) {
return children;
}
const ClientInner = clientExports(
function ClientInner({children}) {
return JSON.stringify(children);
},
'2',
'/inner.js',
new Promise(resolve => (resolveInnerClientComponentChunk = resolve)),
);
const value = {};
function Server() {
return (
<ClientOuter value={value}>
<PassthroughServerComponent>
<ClientInner>{value}</ClientInner>
</PassthroughServerComponent>
</ClientOuter>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveInnerClientComponentChunk();
resolveOuterClientComponentChunk();
});
expect(container.innerHTML).toBe('{}');
});
it('should resolve deduped objects in blocked models referencing other blocked models with blocked references', async () => {
let resolveFooClientComponentChunk;
let resolveBarClientComponentChunk;
function PassthroughServerComponent({children}) {
return children;
}
const FooClient = clientExports(
function FooClient({children}) {
return JSON.stringify(children);
},
'1',
'/foo.js',
new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
);
const BarClient = clientExports(
function BarClient() {
return 'not used';
},
'2',
'/bar.js',
new Promise(resolve => (resolveBarClientComponentChunk = resolve)),
);
const shared = {foo: 1};
function Server() {
return (
<>
<PassthroughServerComponent>
<FooClient key="first" bar={BarClient}>
{shared}
</FooClient>
</PassthroughServerComponent>
<FooClient key="second" bar={BarClient}>
{shared}
</FooClient>
</>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveFooClientComponentChunk();
resolveBarClientComponentChunk();
});
expect(container.innerHTML).toBe('{"foo":1}{"foo":1}');
});
it('should handle deduped props of re-used elements in fragments (same-chunk reference)', async () => {
let resolveFooClientComponentChunk;
const FooClient = clientExports(
function Foo({children, item}) {
return children;
},
'1',
'/foo.js',
new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
);
const shared = <div />;
function Server() {
return (
<FooClient track={shared}>
<>{shared}</>
</FooClient>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveFooClientComponentChunk();
});
expect(container.innerHTML).toBe('<div></div>');
});
it('should handle deduped props of re-used elements in server components (cross-chunk reference)', async () => {
let resolveFooClientComponentChunk;
function PassthroughServerComponent({children}) {
return children;
}
const FooClient = clientExports(
function Foo({children, item}) {
return children;
},
'1',
'/foo.js',
new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
);
const shared = <div />;
function Server() {
return (
<FooClient track={shared}>
<PassthroughServerComponent>{shared}</PassthroughServerComponent>
</FooClient>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveFooClientComponentChunk();
});
expect(container.innerHTML).toBe('<div></div>');
});
it('should handle references to deduped owner objects', async () => {
let path1a: React.ReactNode;
let path1b: React.ReactNode;
let path2: React.ReactNode;
function Svg1() {
return ReactServer.createElement(
'svg',
{id: '1'},
path1a || (path1a = ReactServer.createElement('path', {})),
path1b || (path1b = ReactServer.createElement('path', {})),
);
}
function Svg2() {
return ReactServer.createElement(
'svg',
{id: '2'},
path2 || (path2 = ReactServer.createElement('path', {})),
);
}
function Server() {
return ReactServer.createElement(
ReactServer.Fragment,
{},
ReactServer.createElement(Svg1),
ReactServer.createElement(Svg2),
);
}
let stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
let response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
const expectedHtml =
'<svg id="1"><path></path><path></path></svg><svg id="2"><path></path></svg>';
expect(container.innerHTML).toBe(expectedHtml);
stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
response = ReactServerDOMClient.createFromReadableStream(stream);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe(expectedHtml);
if (__DEV__) {
const resolvedPath1b = response.value[0].props.children[1];
expect(resolvedPath1b._owner).toEqual(
expect.objectContaining({
name: 'Svg1',
env: 'Server',
key: null,
owner: expect.objectContaining({
name: 'Server',
env: 'Server',
key: null,
}),
}),
);
const resolvedPath2 = response.value[1].props.children;
expect(resolvedPath2._owner).toEqual(
expect.objectContaining({
name: 'Svg2',
env: 'Server',
key: null,
owner: expect.objectContaining({
name: 'Server',
env: 'Server',
key: null,
}),
}),
);
}
});
it('should progressively reveal server components', async () => {
let reportedErrors = [];
class ErrorBoundary extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}
let errorBoundaryFn;
if (__DEV__) {
errorBoundaryFn = e => (
<p>
{e.message} + {e.digest}
</p>
);
} else {
errorBoundaryFn = e => {
expect(e.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
' may provide additional details about the nature of the error.',
);
return <p>{e.digest}</p>;
};
}
function MyErrorBoundary({children}) {
return (
<ErrorBoundary fallback={errorBoundaryFn}>{children}</ErrorBoundary>
);
}
function Text({children}) {
return children;
}
const [Friends, resolveFriends] = makeDelayedText(Text);
const [Name, resolveName] = makeDelayedText(Text);
const [Posts, resolvePosts] = makeDelayedText(Text);
const [Photos, resolvePhotos] = makeDelayedText(Text);
const [Games, , rejectGames] = makeDelayedText(Text);
function ProfileDetails({avatar}) {
return (
<div>
<Name>:name:</Name>
{avatar}
</div>
);
}
function ProfileSidebar({friends}) {
return (
<div>
<Photos>:photos:</Photos>
{friends}
</div>
);
}
function ProfilePosts({posts}) {
return <div>{posts}</div>;
}
function ProfileGames({games}) {
return <div>{games}</div>;
}
const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
function ProfileContent() {
return (
<>
<ProfileDetails avatar={<Text>:avatar:</Text>} />
<Suspense fallback={<p>(loading sidebar)</p>}>
<ProfileSidebar friends={<Friends>:friends:</Friends>} />
</Suspense>
<Suspense fallback={<p>(loading posts)</p>}>
<ProfilePosts posts={<Posts>:posts:</Posts>} />
</Suspense>
<MyErrorBoundaryClient>
<Suspense fallback={<p>(loading games)</p>}>
<ProfileGames games={<Games>:games:</Games>} />
</Suspense>
</MyErrorBoundaryClient>
</>
);
}
const model = {
rootContent: <ProfileContent />,
};
function ProfilePage({response}) {
return use(response).rootContent;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
onError(x) {
reportedErrors.push(x);
return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
},
}),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Suspense fallback={<p>(loading)</p>}>
<ProfilePage response={response} />
</Suspense>,
);
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await serverAct(async () => {
await act(() => {
resolveFriends();
});
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await serverAct(async () => {
await act(() => {
resolveName();
});
});
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(() => {
rejectGames(theError);
});
});
const gamesExpectedValue = __DEV__
? '<p>Game over + a dev digest</p>'
: '<p>digest("Game over")</p>';
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
'<p>(loading posts)</p>' +
gamesExpectedValue,
);
expect(reportedErrors).toEqual([theError]);
reportedErrors = [];
await serverAct(async () => {
await act(() => {
resolvePhotos();
});
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<p>(loading posts)</p>' +
gamesExpectedValue,
);
await serverAct(async () => {
await act(() => {
resolvePosts();
});
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<div>:posts:</div>' +
gamesExpectedValue,
);
expect(reportedErrors).toEqual([]);
});
it('should close the stream upon completion when rendering to W3C streams', async () => {
function Text({children}) {
return children;
}
const [Friends, resolveFriends] = makeDelayedText(Text);
const [Name, resolveName] = makeDelayedText(Text);
const [Posts, resolvePosts] = makeDelayedText(Text);
const [Photos, resolvePhotos] = makeDelayedText(Text);
function ProfileDetails({avatar}) {
return (
<div>
<Name>:name:</Name>
{avatar}
</div>
);
}
function ProfileSidebar({friends}) {
return (
<div>
<Photos>:photos:</Photos>
{friends}
</div>
);
}
function ProfilePosts({posts}) {
return <div>{posts}</div>;
}
function ProfileContent() {
return (
<Suspense fallback="(loading everything)">
<ProfileDetails avatar={<Text>:avatar:</Text>} />
<Suspense fallback={<p>(loading sidebar)</p>}>
<ProfileSidebar friends={<Friends>:friends:</Friends>} />
</Suspense>
<Suspense fallback={<p>(loading posts)</p>}>
<ProfilePosts posts={<Posts>:posts:</Posts>} />
</Suspense>
</Suspense>
);
}
const model = {
rootContent: <ProfileContent />,
};
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(model, webpackMap),
);
const reader = stream.getReader();
const decoder = new TextDecoder();
let flightResponse = '';
let isDone = false;
reader.read().then(function progress({done, value}) {
if (done) {
isDone = true;
return;
}
flightResponse += decoder.decode(value);
return reader.read().then(progress);
});
jest.advanceTimersByTime(500);
await serverAct(() => {});
expect(flightResponse).toContain('(loading everything)');
expect(flightResponse).toContain('(loading sidebar)');
expect(flightResponse).toContain('(loading posts)');
if (!__DEV__) {
expect(flightResponse).not.toContain(':friends:');
expect(flightResponse).not.toContain(':name:');
}
await serverAct(() => {
resolveFriends();
});
expect(flightResponse).toContain(':friends:');
await serverAct(() => {
resolveName();
});
expect(flightResponse).toContain(':name:');
await serverAct(() => {
resolvePhotos();
});
expect(flightResponse).toContain(':photos:');
await serverAct(() => {
resolvePosts();
});
expect(flightResponse).toContain(':posts:');
expect(isDone).toBeTruthy();
});
it('should be able to complete after aborting and throw the reason client-side', async () => {
const reportedErrors = [];
let errorBoundaryFn;
if (__DEV__) {
errorBoundaryFn = e => (
<p>
{e.message} + {e.digest}
</p>
);
} else {
errorBoundaryFn = e => {
expect(e.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
' may provide additional details about the nature of the error.',
);
return <p>{e.digest}</p>;
};
}
class ErrorBoundary extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}
const controller = new AbortController();
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<div>
<InfiniteSuspend />
</div>,
webpackMap,
{
signal: controller.signal,
onError(x) {
const message = typeof x === 'string' ? x : x.message;
reportedErrors.push(x);
return __DEV__ ? 'a dev digest' : `digest("${message}")`;
},
},
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App({res}) {
return use(res);
}
await act(() => {
root.render(
<ErrorBoundary fallback={errorBoundaryFn}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>(loading)</p>');
await act(() => {
controller.abort('for reasons');
});
const expectedValue = __DEV__
? '<p>for reasons + a dev digest</p>'
: '<p>digest("for reasons")</p>';
expect(container.innerHTML).toBe(expectedValue);
expect(reportedErrors).toEqual(['for reasons']);
});
it('should warn in DEV a child is missing keys', async () => {
function ParentClient({children}) {
return children;
}
const Parent = clientExports(ParentClient);
const ParentModule = clientExports({Parent: ParentClient});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<>
<Parent>{Array(6).fill(<div>no key</div>)}</Parent>
<ParentModule.Parent>
{Array(6).fill(<div>no key</div>)}
</ParentModule.Parent>
</>,
webpackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
await act(() => {
root.render(result);
});
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the top-level render call using <ParentClient>. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in div (at **)',
]);
});
it('basic use(promise)', async () => {
function Server() {
return (
ReactServer.use(Promise.resolve('A')) +
ReactServer.use(Promise.resolve('B')) +
ReactServer.use(Promise.resolve('C'))
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Client />
</Suspense>,
);
});
expect(container.innerHTML).toBe('ABC');
});
it('use(promise) in multiple components', async () => {
function Child({prefix}) {
return (
prefix +
ReactServer.use(Promise.resolve('C')) +
ReactServer.use(Promise.resolve('D'))
);
}
function Parent() {
return (
<Child
prefix={
ReactServer.use(Promise.resolve('A')) +
ReactServer.use(Promise.resolve('B'))
}
/>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Parent />),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Client />
</Suspense>,
);
});
expect(container.innerHTML).toBe('ABCD');
});
it('using a rejected promise will throw', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.reject(new Error('Oops!'));
const promiseC = Promise.resolve('C');
await expect(promiseB).rejects.toThrow('Oops!');
function Server() {
return (
ReactServer.use(promiseA) +
ReactServer.use(promiseB) +
ReactServer.use(promiseC)
);
}
const reportedErrors = [];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap, {
onError(x) {
reportedErrors.push(x);
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
}),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return __DEV__
? this.state.error.message + ' + ' + this.state.error.digest
: this.state.error.digest;
}
return this.props.children;
}
}
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<ErrorBoundary>
<Client />
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe(
__DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
);
expect(reportedErrors.length).toBe(1);
expect(reportedErrors[0].message).toBe('Oops!');
});
it("use a promise that's already been instrumented and resolved", async () => {
const thenable = {
status: 'fulfilled',
value: 'Hi',
then() {},
};
function Server() {
return ReactServer.use(thenable);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Client />);
});
expect(container.innerHTML).toBe('Hi');
});
it('unwraps thenable that fulfills synchronously without suspending', async () => {
function Server() {
const thenable = {
then(resolve) {
resolve('Hi');
},
};
try {
return ReactServer.use(thenable);
} catch {
throw new Error(
'`use` should not suspend because the thenable resolved synchronously.',
);
}
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Client />);
});
expect(container.innerHTML).toBe('Hi');
});
it('can pass a higher order function by reference from server to client', async () => {
let actionProxy;
function Client({action}) {
actionProxy = action;
return 'Click Me';
}
function greet(transform, text) {
return 'Hello ' + transform(text);
}
function upper(text) {
return text.toUpperCase();
}
const ServerModuleA = serverExports({
greet,
});
const ServerModuleB = serverExports({
upper,
});
const ClientRef = clientExports(Client);
const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef action={boundFn} />,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(ref, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(ref, body);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(container.innerHTML).toBe('Click Me');
expect(typeof actionProxy).toBe('function');
expect(actionProxy).not.toBe(boundFn);
const result = await actionProxy('hi');
expect(result).toBe('Hello HI');
});
it('can call a module split server function', async () => {
let actionProxy;
function Client({action}) {
actionProxy = action;
return 'Click Me';
}
function greet(text) {
return 'Hello ' + text;
}
const ServerModule = serverExports({
split: greet,
});
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef action={ServerModule.split} />,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(ref, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(ref, body);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(container.innerHTML).toBe('Click Me');
expect(typeof actionProxy).toBe('function');
const result = await actionProxy('Split');
expect(result).toBe('Hello Split');
});
it('can pass a server function by importing from client back to server', async () => {
function greet(transform, text) {
return 'Hello ' + transform(text);
}
function upper(text) {
return text.toUpperCase();
}
const ServerModuleA = serverExports({
greet,
});
const ServerModuleB = serverExports({
upper,
});
let actionProxy;
const ServerModuleBImportedOnClient = {
upper: ReactServerDOMClient.createServerReference(
ServerModuleB.upper.$$id,
async function (ref, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(ref, body);
},
undefined,
undefined,
'upper',
),
};
expect(ServerModuleBImportedOnClient.upper.name).toBe(
__DEV__ ? 'upper' : 'action',
);
if (__DEV__) {
expect(ServerModuleBImportedOnClient.upper.toString()).toBe(
'(...args) => server(...args)',
);
}
function Client({action}) {
actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
return 'Click Me';
}
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef action={ServerModuleA.greet} />,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(ref, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(ref, body);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(container.innerHTML).toBe('Click Me');
const result = await actionProxy('hi');
expect(result).toBe('Hello HI');
});
it('can bind arguments to a server reference', async () => {
let actionProxy;
function Client({action}) {
actionProxy = action;
return 'Click Me';
}
const greet = serverExports(function greet(a, b, c) {
return a + ' ' + b + c;
});
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef action={greet.bind(null, 'Hello').bind(null, 'World')} />,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(actionId, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(actionId, body);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(container.innerHTML).toBe('Click Me');
expect(typeof actionProxy).toBe('function');
expect(actionProxy).not.toBe(greet);
const result = await actionProxy('!');
expect(result).toBe('Hello World!');
});
it('propagates server reference errors to the client', async () => {
let actionProxy;
function Client({action}) {
actionProxy = action;
return 'Click Me';
}
async function send(text) {
throw new Error(`Error for ${text}`);
}
const ServerModule = serverExports({send});
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef action={ServerModule.send} />,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(actionId, args) {
const body = await ReactServerDOMClient.encodeReply(args);
const result = callServer(actionId, body);
result.catch(() => {});
return ReactServerDOMClient.createFromReadableStream(
ReactServerDOMServer.renderToReadableStream(result, null, {
onError: error => 'test-error-digest',
}),
);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
let thrownError;
try {
await serverAct(() => actionProxy('test'));
} catch (error) {
thrownError = error;
}
if (__DEV__) {
expect(thrownError).toEqual(new Error('Error for test'));
} else {
expect(thrownError).toEqual(
new Error(
'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
),
);
expect(thrownError.digest).toBe('test-error-digest');
}
});
it('can use the same function twice as a server action', async () => {
let actionProxy1;
let actionProxy2;
function Client({action1, action2}) {
actionProxy1 = action1;
actionProxy2 = action2;
return 'Click Me';
}
function greet(text) {
return 'Hello ' + text;
}
const ServerModule = serverExports({
greet,
greet2: greet,
});
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientRef
action1={ServerModule.greet}
action2={ServerModule.greet2}
/>,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(ref, args) {
const body = await ReactServerDOMClient.encodeReply(args);
return callServer(ref, body);
},
});
function App() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(container.innerHTML).toBe('Click Me');
expect(typeof actionProxy1).toBe('function');
expect(actionProxy1).not.toBe(greet);
expect(actionProxy1).not.toBe(actionProxy2);
const result = await actionProxy1('world');
expect(result).toBe('Hello world');
});
it('can pass an async server exports that resolves later to an outline object like a Map', async () => {
let resolve;
const chunkPromise = new Promise(r => (resolve = r));
function action() {}
const serverModule = serverExports(
{
action: action,
},
chunkPromise,
);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{action: serverModule.action},
webpackMap,
),
);
const response =
await ReactServerDOMClient.createFromReadableStream(stream);
const map = new Map();
map.set('action', response.action);
const body = await ReactServerDOMClient.encodeReply(map);
const resultPromise = ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
await resolve();
const result = await resultPromise;
expect(result instanceof Map).toBe(true);
expect(result.get('action')).toBe(action);
});
it('supports Float hints before the first await in server components in Fiber', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
ReactServerDOM.preload('before', {as: 'style'});
await 1;
ReactServerDOM.preload('after', {as: 'style'});
return <ClientComponent />;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ServerComponent />,
webpackMap,
),
);
let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(stream);
}
return response;
}
function App() {
return getResponse();
}
await 1;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
expect(document.head.innerHTML).toBe(
gate(f => f.www)
?
'<link rel="preload" href="after" as="style"><link rel="preload" href="before" as="style">'
:
'<link rel="preload" href="before" as="style">',
);
expect(container.innerHTML).toBe('<p>hello world</p>');
});
it('Does not support Float hints in server components anywhere in Fizz', async () => {
function Component() {
return <p>hello world</p>;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
ReactDOM.preload('before', {as: 'style'});
await 1;
ReactDOM.preload('after', {as: 'style'});
return <ClientComponent />;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ServerComponent />,
webpackMap,
),
);
let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(stream);
}
return response;
}
function App() {
return (
<html>
<body>{getResponse()}</body>
</html>
);
}
let fizzPromise;
await act(async () => {
fizzPromise = ReactDOMFizzServer.renderToReadableStream(<App />);
});
const fizzStream = await fizzPromise;
const decoder = new TextDecoder();
const reader = fizzStream.getReader();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
content += decoder.decode();
break;
}
content += decoder.decode(value, {stream: true});
}
expect(content).toEqual(
'<!DOCTYPE html><html><head>' +
(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 postpone in Server Components', async () => {
function Server() {
React.unstable_postpone('testing postpone');
return 'Not shown';
}
let postponed = null;
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<Suspense fallback="Loading...">
<Server />
</Suspense>,
null,
{
onPostpone(reason) {
postponed = reason;
},
},
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<div>
Shell: <Client />
</div>,
);
});
expect(container.innerHTML).toContain('Shell:');
expect(container.innerHTML).toContain('Loading...');
expect(container.innerHTML).not.toContain('Not shown');
expect(postponed).toBe('testing postpone');
});
it('should not continue rendering after the reader cancels', async () => {
let hasLoaded = false;
let resolve;
let rendered = false;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
rendered = true;
return 'Done';
}
const errors = [];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<Wait />
</Suspense>
</div>,
null,
{
onError(x) {
errors.push(x.message);
},
},
),
);
expect(rendered).toBe(false);
const reader = stream.getReader();
await reader.read();
await reader.cancel();
expect(errors).toEqual([
'The render was aborted by the server without a reason.',
]);
hasLoaded = true;
resolve();
await jest.runAllTimers();
expect(rendered).toBe(false);
expect(errors).toEqual([
'The render was aborted by the server without a reason.',
]);
});
it('postpones when abort passes a postpone signal', async () => {
const infinitePromise = new Promise(() => {});
function Server() {
return infinitePromise;
}
let postponed = null;
let error = null;
const controller = new AbortController();
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<Suspense fallback="Loading...">
<Server />
</Suspense>,
null,
{
onError(x) {
error = x;
},
onPostpone(reason) {
postponed = reason;
},
signal: controller.signal,
},
),
);
try {
React.unstable_postpone('testing postpone');
} catch (reason) {
controller.abort(reason);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
Shell: <Client />
</div>,
);
});
expect(container.innerHTML).toContain('Shell:');
expect(container.innerHTML).toContain('Loading...');
expect(container.innerHTML).not.toContain('Not shown');
expect(postponed).toBe('testing postpone');
expect(error).toBe(null);
});
function passThrough(stream) {
return new ReadableStream({
async start(controller) {
const reader = stream.getReader();
function push() {
reader.read().then(({done, value}) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
push();
return;
});
}
push();
},
});
}
it('should supports streaming ReadableStream with objects', async () => {
const errors = [];
let controller1;
let controller2;
const s1 = new ReadableStream({
start(c) {
controller1 = c;
},
});
const s2 = new ReadableStream({
start(c) {
controller2 = c;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
s1,
s2,
},
{},
{
onError(x) {
errors.push(x);
return x;
},
},
),
);
const result = await ReactServerDOMClient.createFromReadableStream(
passThrough(rscStream),
);
const reader1 = result.s1.getReader();
const reader2 = result.s2.getReader();
await serverAct(() => {
controller1.enqueue({hello: 'world'});
controller2.enqueue({hi: 'there'});
});
expect(await reader1.read()).toEqual({
value: {hello: 'world'},
done: false,
});
expect(await reader2.read()).toEqual({
value: {hi: 'there'},
done: false,
});
await serverAct(async () => {
controller1.enqueue('text1');
controller2.enqueue('text2');
controller1.close();
});
expect(await reader1.read()).toEqual({
value: 'text1',
done: false,
});
expect(await reader1.read()).toEqual({
value: undefined,
done: true,
});
expect(await reader2.read()).toEqual({
value: 'text2',
done: false,
});
await serverAct(async () => {
controller2.error('rejected');
});
let error = null;
try {
await reader2.read();
} catch (x) {
error = x;
}
expect(error.digest).toBe('rejected');
expect(errors).toEqual(['rejected']);
});
it('should cancels the underlying ReadableStream when we are cancelled', async () => {
let controller;
let cancelReason;
const s = new ReadableStream({
start(c) {
controller = c;
},
cancel(r) {
cancelReason = r;
},
});
let loggedReason;
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
s,
{},
{
onError(reason) {
loggedReason = reason;
},
},
),
);
const reader = rscStream.getReader();
controller.enqueue('hi');
const reason = new Error('aborted');
reader.cancel(reason);
await reader.read();
expect(cancelReason).toBe(reason);
expect(loggedReason).toBe(reason);
});
it('should cancels the underlying ReadableStream when we abort', async () => {
const errors = [];
let controller;
let cancelReason;
const abortController = new AbortController();
const s = new ReadableStream({
start(c) {
controller = c;
},
cancel(r) {
cancelReason = r;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
s,
{},
{
signal: abortController.signal,
onError(x) {
errors.push(x);
return x.message;
},
},
),
);
const result = await ReactServerDOMClient.createFromReadableStream(
passThrough(rscStream),
);
const reader = result.getReader();
controller.enqueue('hi');
await 0;
const reason = new Error('aborted');
abortController.abort(reason);
expect(await reader.read()).toEqual({
value: 'hi',
done: false,
});
expect(cancelReason).toBe(reason);
let error = null;
try {
await reader.read();
} catch (x) {
error = x;
}
expect(error.digest).toBe('aborted');
expect(errors).toEqual([reason]);
});
it('should supports streaming AsyncIterables with objects', async () => {
let resolve;
const wait = new Promise(r => (resolve = r));
const errors = [];
const multiShotIterable = {
async *[Symbol.asyncIterator]() {
const next = yield {hello: 'A'};
expect(next).toBe(undefined);
await wait;
yield {hi: 'B'};
return 'C';
},
};
const singleShotIterator = (async function* () {
const next = yield {hello: 'D'};
expect(next).toBe(undefined);
await wait;
yield {hi: 'E'};
throw 'F';
})();
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
multiShotIterable,
singleShotIterator,
},
{},
{
onError(x) {
errors.push(x);
return x;
},
},
),
);
const result = await ReactServerDOMClient.createFromReadableStream(
passThrough(rscStream),
);
const iterator1 = result.multiShotIterable[Symbol.asyncIterator]();
const iterator2 = result.singleShotIterator[Symbol.asyncIterator]();
expect(iterator1).not.toBe(result.multiShotIterable);
expect(iterator2).toBe(result.singleShotIterator);
expect(await iterator1.next()).toEqual({
value: {hello: 'A'},
done: false,
});
expect(await iterator2.next()).toEqual({
value: {hello: 'D'},
done: false,
});
await serverAct(() => {
resolve();
});
expect(await iterator1.next()).toEqual({
value: {hi: 'B'},
done: false,
});
expect(await iterator2.next()).toEqual({
value: {hi: 'E'},
done: false,
});
expect(await iterator1.next()).toEqual({
value: 'C',
done: true,
});
expect(await iterator1.next()).toEqual({
value: undefined,
done: true,
});
let error = null;
try {
await iterator2.next();
} catch (x) {
error = x;
}
expect(error.digest).toBe('F');
expect(errors).toEqual(['F']);
const iterator3 = result.multiShotIterable[Symbol.asyncIterator]();
expect(iterator3).not.toBe(iterator1);
expect(iterator3.next().value).toEqual({
value: {hello: 'A'},
done: false,
});
expect(iterator3.next().value).toEqual({
value: {hi: 'B'},
done: false,
});
expect(iterator3.next().value).toEqual({
value: 'C',
done: true,
});
expect(iterator3.next().value).toEqual({
value: undefined,
done: true,
});
expect(() => iterator3.next('this is not allowed')).toThrow(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
);
});
it('should cancels the underlying AsyncIterable when we are cancelled', async () => {
let resolve;
const wait = new Promise(r => (resolve = r));
let thrownReason;
const iterator = (async function* () {
try {
await wait;
yield 'a';
yield 'b';
} catch (x) {
thrownReason = x;
}
yield 'c';
})();
let loggedReason;
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
iterator,
{},
{
onError(reason) {
loggedReason = reason;
},
},
),
);
const reader = rscStream.getReader();
const reason = new Error('aborted');
reader.cancel(reason);
await resolve();
await reader.read();
expect(thrownReason).toBe(reason);
expect(loggedReason).toBe(reason);
});
it('should cancels the underlying AsyncIterable when we abort', async () => {
const errors = [];
const abortController = new AbortController();
let resolve;
const wait = new Promise(r => (resolve = r));
let thrownReason;
const iterator = (async function* () {
try {
yield 'a';
await wait;
yield 'b';
} catch (x) {
thrownReason = x;
}
yield 'c';
})();
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
iterator,
{},
{
signal: abortController.signal,
onError(x) {
errors.push(x);
return x.message;
},
},
),
);
const result = await ReactServerDOMClient.createFromReadableStream(
passThrough(rscStream),
);
const reason = new Error('aborted');
abortController.abort(reason);
await serverAct(() => {
resolve();
});
expect(await result.next()).toEqual({
value: 'a',
done: false,
});
expect(thrownReason).toBe(reason);
let error = null;
try {
await result.next();
} catch (x) {
error = x;
}
expect(error.digest).toBe('aborted');
expect(errors).toEqual([reason]);
});
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.unstable_prerender(
<App />,
webpackMap,
),
};
});
resolveGreeting();
const {prelude} = await pendingResult;
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(
passThrough(prelude),
);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('<div>hello world</div>');
});
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.unstable_prerender(
<App />,
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});
controller.abort('boom');
resolveGreeting();
const {prelude} = await serverAct(() => pendingResult);
expect(errors).toEqual([]);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(
passThrough(prelude),
);
const container = document.createElement('div');
errors.length = 0;
const root = ReactDOMClient.createRoot(container, {
onUncaughtError(err) {
errors.push(err);
},
});
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(errors).toEqual([new Error('Connection closed.')]);
expect(container.innerHTML).toBe('');
});
it('can dedupe references inside promises', async () => {
const foo = {};
const bar = {
foo: foo,
};
foo.bar = bar;
const object = {
foo: Promise.resolve(foo),
bar: Promise.resolve(bar),
};
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(object, webpackMap),
);
const response = await ReactServerDOMClient.createFromReadableStream(
passThrough(stream),
);
const responseFoo = await response.foo;
const responseBar = await response.bar;
expect(responseFoo.bar).toBe(responseBar);
expect(responseBar.foo).toBe(responseFoo);
});
it('can deduped outlined references inside promises', async () => {
const foo = {};
const bar = new Set([foo]);
foo.bar = bar;
const object = {
foo: Promise.resolve(foo),
bar: Promise.resolve(bar),
};
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(object, webpackMap),
);
const response = await ReactServerDOMClient.createFromReadableStream(
passThrough(stream),
);
const responseFoo = await response.foo;
const responseBar = await response.bar;
expect(responseFoo.bar).toBe(responseBar);
expect(Array.from(responseBar)[0]).toBe(responseFoo);
});
it('should resolve deduped references in maps used in client component props', async () => {
const ClientComponent = clientExports(function ClientComponent({
shared,
map,
}) {
expect(map.get(42)).toBe(shared);
return JSON.stringify({shared, map: Array.from(map)});
});
function Server() {
const shared = {id: 42};
const map = new Map([[42, shared]]);
return <ClientComponent shared={shared} map={map} />;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe(
'{"shared":{"id":42},"map":[[42,{"id":42}]]}',
);
});
});