/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.WritableStream =
require('web-streams-polyfill/ponyfill/es6').WritableStream;
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 getDebugInfo;
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;
getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
ignoreProps: true,
useFixedTime: true,
});
// Simulate the condition resolution
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');
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 {children};
}
return [DelayedText, _resolve, _reject];
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}
function requireServerRef(ref) {
let name = '';
let resolvedModuleData = webpackServerMap[ref];
if (resolvedModuleData) {
// The potentially aliased name.
name = resolvedModuleData.name;
} else {
// We didn't find this specific export name but we might have the * export
// which contains this name as well.
// TODO: It's unfortunate that we now have to parse this string. We should
// probably go back to encoding path and name separately on the client reference.
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);
}
function createDelayedStream(
stream: ReadableStream,
): ReadableStream {
return new ReadableStream({
async start(controller) {
const reader = stream.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
controller.close();
} else {
// Artificially delay between enqueuing chunks.
await new Promise(resolve => setTimeout(resolve));
controller.enqueue(value);
}
}
},
});
}
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
it('should resolve HTML using W3C streams', async () => {
function Text({children}) {
return {children};
}
function HTML() {
return (
');
});
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 ;
});
const ClientInner = clientExports(
function ClientInner({value}) {
return
{JSON.stringify(value)}
;
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function Server({value}) {
return ;
}
const shared = [1, 2, 3];
const value = [shared, shared];
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
,
webpackMap,
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream);
// make sure we have a listener so that `resolveModelChunk` initializes the chunk eagerly
response.then(() => {});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveClientComponentChunk();
});
expect(container.innerHTML).toBe('
');
});
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 (
{value}
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, 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();
});
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 (
<>
{shared}
{shared}
>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, 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();
});
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 = ;
function Server() {
return (
<>{shared}>
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, 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();
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveFooClientComponentChunk();
});
expect(container.innerHTML).toBe('');
});
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 = ;
function Server() {
return (
{shared}
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, 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();
});
expect(container.innerHTML).toBe('');
await act(() => {
resolveFooClientComponentChunk();
});
expect(container.innerHTML).toBe('');
});
it('should handle references to deduped owner objects', async () => {
// This is replicating React components as generated by @svgr/webpack:
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(, 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();
});
const expectedHtml =
'';
expect(container.innerHTML).toBe(expectedHtml);
// Render a second time:
// Assigning the path elements to variables in module scope (here simulated
// with the test's function scope), and rendering a second time, prevents
// the owner of the path elements (i.e. Svg1/Svg2) to be deduped. The owner
// of the path in Svg1 is fully inlined. The owner of the owner of the path
// in Svg2 is Server, which is deduped and replaced with a reference to the
// owner of the owner of the path in Svg1. This nested owner is actually
// Server from the previous render pass, which is kinda broken and libraries
// probably shouldn't generate code like this. This reference can only be
// resolved properly if owners are specifically handled when resolving
// outlined models.
stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, webpackMap),
);
response = ReactServerDOMClient.createFromReadableStream(stream);
await act(() => {
root.render();
});
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 = [];
// Client Components
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 => (
{e.message} + {e.digest}
);
} 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
');
// This isn't enough to show anything.
await serverAct(async () => {
await act(() => {
resolveFriends();
});
});
expect(container.innerHTML).toBe('
(loading)
');
// We can now show the details. Sidebar and posts are still loading.
await serverAct(async () => {
await act(() => {
resolveName();
});
});
// Advance time enough to trigger a nested fallback.
jest.advanceTimersByTime(500);
expect(container.innerHTML).toBe(
'
' +
gamesExpectedValue,
);
expect(reportedErrors).toEqual([]);
});
it('should close the stream upon completion when rendering to W3C streams', async () => {
// Model
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);
// View
function ProfileDetails({avatar}) {
return (
:name:
{avatar}
);
}
function ProfileSidebar({friends}) {
return (
:photos:
{friends}
);
}
function ProfilePosts({posts}) {
return
{posts}
;
}
function ProfileContent() {
return (
:avatar:} />
(loading sidebar)}>
:friends:} />
(loading posts)}>
:posts:} />
);
}
const model = {
rootContent: ,
};
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);
});
// Advance time enough to trigger a nested fallback.
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:');
// Final pending chunk is written; stream should be closed.
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 => (
{e.message} + {e.digest}
);
} 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
';
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(
<>
{Array(6).fill(
no key
)}
{Array(6).fill(
no key
)}
>,
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 . ' +
'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(),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
,
);
});
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 (
);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
,
);
});
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');
// Jest/Node will raise an unhandled rejected error unless we await this. It
// works fine in the browser, though.
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(, 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(
,
);
});
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() {},
};
// This will never suspend because the thenable already resolved
function Server() {
return ReactServer.use(thenable);
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.innerHTML).toBe('Hi');
});
it('unwraps thenable that fulfills synchronously without suspending', async () => {
function Server() {
const thenable = {
then(resolve) {
// This thenable immediately resolves, synchronously, without waiting
// a microtask.
resolve('Hi');
},
};
try {
return ReactServer.use(thenable);
} catch {
throw new Error(
'`use` should not suspend because the thenable resolved synchronously.',
);
}
}
// Because the thenable resolves synchronously, we should be able to finish
// rendering synchronously, with no fallback.
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(),
);
const response = ReactServerDOMClient.createFromReadableStream(stream);
function Client() {
return use(response);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
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(
,
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();
});
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({
// This gets split into another module
split: greet,
});
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
,
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();
});
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;
// This is a Proxy representing ServerModuleB in the Client bundle.
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}) {
// Client side pass a Server Reference into an action.
actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
return 'Click Me';
}
const ClientRef = clientExports(Client);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
,
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();
});
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(
,
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();
});
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(
,
webpackMap,
),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
async callServer(actionId, args) {
const body = await ReactServerDOMClient.encodeReply(args);
const result = callServer(actionId, body);
// Flight doesn't attach error handlers early enough. we suppress the warning
// by putting a dummy catch on the result here
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();
});
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(
,
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();
});
expect(container.innerHTML).toBe('Click Me');
expect(typeof actionProxy1).toBe('function');
expect(actionProxy1).not.toBe(greet);
// TODO: Ideally flight would be encoding this the same.
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,
);
// Send the action to the client
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{action: serverModule.action},
webpackMap,
),
);
// Snapshot updates change this formatting, so we let prettier ignore it.
// prettier-ignore
const response =
await ReactServerDOMClient.createFromReadableStream(stream);
// Pass the action back to the server inside a Map
const map = new Map();
map.set('action', response.action);
const body = await ReactServerDOMClient.encodeReply(map);
const resultPromise = ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
// We couldn't yet resolve the server reference because we haven't loaded
// its chunk yet in the new server instance. We now resolve it which loads
// it asynchronously.
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
hello world
;
}
const ClientComponent = clientExports(Component);
async function ServerComponent() {
ReactServerDOM.preload('before', {as: 'style'});
await 1;
ReactServerDOM.preload('after', {as: 'style'});
return ;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
,
webpackMap,
),
);
let response = null;
function getResponse() {
if (response === null) {
response = ReactServerDOMClient.createFromReadableStream(stream);
}
return response;
}
function App() {
return getResponse();
}
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
// we aren't operating separate module graphs for flight and fiber. In a real app
// each would have their own dispatcher and there would be no cross dispatching.
await 1;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(document.head.innerHTML).toBe(
gate(f => f.www)
? // The www entrypoints for ReactDOM and ReactDOMClient are unified so even
// when you pull in just the top level the dispatcher for the Document is
// loaded alongside it. In a normal environment there would be nothing to dispatch to
// in a server environment so the preload calls would still only be dispatched to fizz
// or the browser but not both. However in this contrived test environment the preloads
// are being dispatched simultaneously causing an extraneous preload to show up. This test currently
// asserts this be demonstrating that the preload call after the await point
// is written to the document before the call before it. We still demonstrate that
// flight handled the sync call because if the fiber implementation did it would appear
// before the after call. In the future we will change this assertion once the fiber
// implementation no long automatically gets pulled in
''
: // For other release channels the client and isomorphic entrypoints are separate and thus we only
// observe the expected preload from before the first await
'',
);
expect(container.innerHTML).toBe('
hello world
');
});
it('Does not support Float hints in server components anywhere in Fizz', async () => {
// In environments that do not support AsyncLocalStorage the Flight client has no ability
// to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
// the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
// a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
function Component() {
return
');
});
it('closes inner ReadableStreams gracefully with unstable_allowPartialStream', async () => {
let streamController;
const innerStream = new ReadableStream({
start(c) {
streamController = c;
},
});
const abortController = new AbortController();
const {pendingResult} = await serverAct(async () => {
streamController.enqueue({hello: 'world'});
return {
pendingResult: ReactServerDOMStaticServer.prerender(
{stream: innerStream},
webpackMap,
{
signal: abortController.signal,
},
),
};
});
abortController.abort();
const {prelude} = await serverAct(() => pendingResult);
const response = await ReactServerDOMClient.createFromReadableStream(
passThrough(prelude),
{
unstable_allowPartialStream: true,
},
);
// The inner stream should be readable up to what was enqueued.
const reader = response.stream.getReader();
const {value, done} = await reader.read();
expect(value).toEqual({hello: 'world'});
expect(done).toBe(false);
// The next read should signal the stream is done (closed, not errored).
const final = await reader.read();
expect(final.done).toBe(true);
});
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]); // This will be outlined which can create a future reference
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 ;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, 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();
});
expect(container.innerHTML).toBe(
'{"shared":{"id":42},"map":[[42,{"id":42}]]}',
);
});
it('should resolve a cycle between debug info and the value it produces', async () => {
function Inner({style}) {
return ;
}
function Component({style}) {
return ;
}
const style = {};
const element = ;
style.element = element;
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(element, 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();
});
expect(container.innerHTML).toBe('');
});
it('does not close the response early when using a fast debug channel', async () => {
function Component() {
return
Hi
;
}
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(, webpackMap, {
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
}),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(
// Create a delayed stream to simulate that the RSC stream might be
// transported slower than the debug channel, which must not lead to a
// `Connection closed` error in the Flight client.
createDelayedStream(rscStream),
{
debugChannel: {readable: debugReadableStream},
},
);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.innerHTML).toBe('
Hi
');
});
it('can transport debug info through a dedicated debug channel', async () => {
let ownerStack;
const ClientComponent = clientExports(() => {
ownerStack = React.captureOwnerStack ? React.captureOwnerStack() : null;
return
Hi
;
});
function App() {
return ReactServer.createElement(
ReactServer.Suspense,
null,
ReactServer.createElement(ClientComponent, null),
);
}
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
ReactServer.createElement(App, null),
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
replayConsoleLogs: true,
debugChannel: {
readable: debugReadableStream,
// Explicitly not defining a writable side here. Its presence was
// previously used as a condition to wait for referenced debug chunks.
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
if (__DEV__) {
expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
}
expect(container.innerHTML).toBe('
Hi
');
});
it('should not have missing key warnings when a static child is blocked on debug info', async () => {
const ClientComponent = clientExports(function ClientComponent({element}) {
return (
Hi
{element}
);
});
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
Sebbie} />,
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: createDelayedStream(debugReadableStream)},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
// Wait for the debug info to be processed.
await act(() => {});
expect(container.innerHTML).toBe(
'
HiSebbie
',
);
});
it('should fully resolve debug info when transported through a (slow) debug channel', async () => {
function Paragraph({children}) {
return ReactServer.createElement('p', null, children);
}
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const app = ReactServer.createElement(
ReactServer.Fragment,
null,
ReactServer.createElement(Paragraph, null, 'foo'),
ReactServer.createElement(Paragraph, null, 'bar'),
);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
root: app,
},
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
const {root} = use(response);
return root;
}
const [slowDebugStream1, slowDebugStream2] =
createDelayedStream(debugReadableStream).tee();
const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: slowDebugStream1},
});
const container = document.createElement('div');
const clientRoot = ReactDOMClient.createRoot(container);
await act(() => {
clientRoot.render();
});
if (__DEV__) {
const debugStreamReader = slowDebugStream2.getReader();
while (true) {
const {done} = await debugStreamReader.read();
if (done) {
break;
}
// Allow the client to process each debug chunk as it arrives.
await act(() => {});
}
}
expect(container.innerHTML).toBe('
foo
bar
');
if (
__DEV__ &&
gate(
flags =>
flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
)
) {
const result = await response;
const firstParagraph = result.root[0];
expect(getDebugInfo(firstParagraph)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Paragraph",
"props": {},
"stack": [
[
"Object.",
"/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js",
2989,
19,
2973,
89,
],
],
},
{
"time": 0,
},
]
`);
}
});
it('should resolve a cycle between debug info and the value it produces when using a debug channel', async () => {
// Same as `should resolve a cycle between debug info and the value it produces`, but using a debug channel.
function Inner({style}) {
return ;
}
function Component({style}) {
return ;
}
const style = {};
const element = ;
style.element = element;
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(element, webpackMap, {
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
}),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
debugChannel: {readable: debugReadableStream},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
expect(container.innerHTML).toBe('');
});
// Long enough to exceed MAX_ROW_SIZE in ReactFlightServer, which makes the
// element prop that follows it be outlined into its own row.
const longText = 'a'.repeat(4000);
it('should not have missing key warnings when a static child is outlined', async () => {
const ClientComponent = clientExports(function ClientComponent({
text,
element,
}) {
return (
',
);
});
it('should not have missing key warnings when an outlined static child is blocked on debug info', async () => {
const ClientComponent = clientExports(function ClientComponent({
text,
element,
}) {
return (
{text.length}
{element}
);
});
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
Hello} />,
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: createDelayedStream(debugReadableStream)},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
// Wait for the debug info to be processed.
await act(() => {});
expect(container.innerHTML).toBe(
'
4000Hello
',
);
});
it('should have missing key warnings when an outlined element is used in an array', async () => {
const ClientComponent = clientExports(function ClientComponent({
text,
element,
}) {
return (
{text.length}
{[element]}
);
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
Hello} />,
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();
});
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the render method of `div`. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)',
]);
expect(container.innerHTML).toBe(
'
4000Hello
',
);
});
it('should have missing key warnings when an outlined element that is blocked on debug info is used in an array', async () => {
const ClientComponent = clientExports(function ClientComponent({
text,
element,
}) {
return (
{text.length}
{[element]}
);
});
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
Hello} />,
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: createDelayedStream(debugReadableStream)},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render();
});
// The element can only be rendered, and therefore validated, after it's
// unblocked by the debug info.
await act(() => {});
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the render method of `div`. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)',
]);
expect(container.innerHTML).toBe(
'
4000Hello
',
);
});
describe('with console.createTask', () => {
// Stands in for what a browser console does with fake tasks: whatever runs
// inside a task is shown under that task's name in the async stack. This is
// the same setup that `ReactServer-test` uses to assert on task names.
let currentTask;
beforeEach(() => {
const {AsyncLocalStorage} = require('node:async_hooks');
currentTask = new AsyncLocalStorage();
(console: any).createTask = taskName => ({
run: taskFn => {
const parentTask = currentTask.getStore() || '';
return currentTask.run(parentTask + '\n' + taskName, taskFn);
},
});
// `supportsCreateTask` is captured when ReactFlightClient is required, so
// the client modules need to be required again with this in place.
jest.resetModules();
patchMessageChannel();
({act} = require('internal-test-utils'));
React = require('react');
use = React.use;
ReactDOMClient = require('react-dom/client');
ReactServerDOMClient = require('react-server-dom-webpack/client');
});
afterEach(() => {
delete (console: any).createTask;
});
// @gate __DEV__
it('renders a client component inside a "use client" task', async () => {
let taskWhileRendering;
const ClientComponent = clientExports(function ClientComponent() {
taskWhileRendering = currentTask.getStore();
return Hello;
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
,
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();
});
expect(container.innerHTML).toBe('Hello');
// The element's type is a lazy node wrapping the client reference, so the
// task that the component renders in marks the boundary into the client.
expect(taskWhileRendering).toBe('\n"use client"');
});
});
});