/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate'; import {Readable} from 'stream'; // Polyfills for test environment global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream; global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; let act; let serverAct; let use; let clientExports; let clientExportsESM; let clientModuleError; let webpackMap; let Stream; let FlightReact; let React; let FlightReactDOM; let ReactDOMClient; let ReactServerDOMServer; let ReactServerDOMStaticServer; let ReactServerDOMClient; let ReactDOMFizzServer; let Suspense; let ErrorBoundary; let JSDOM; let assertConsoleErrorDev; describe('ReactFlightDOM', () => { beforeEach(() => { // For this first reset we are going to load the dom-node version of react-server-dom-webpack/server // This can be thought of as essentially being the React Server Components scope with react-server // condition jest.resetModules(); // Some of the tests pollute the head. document.head.innerHTML = ''; JSDOM = require('jsdom').JSDOM; patchSetImmediate(); serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); FlightReact = require('react'); FlightReactDOM = require('react-dom'); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-unbundled/server.node'), ); jest.mock('react-server-dom-webpack/static', () => require('react-server-dom-unbundled/static.node'), ); const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; clientExportsESM = WebpackMock.clientExportsESM; clientModuleError = WebpackMock.clientModuleError; webpackMap = WebpackMock.webpackMap; ReactServerDOMServer = require('react-server-dom-webpack/server'); ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); // This reset is to load modules for the SSR/Browser scope. jest.unmock('react-server-dom-webpack/server'); __unmockReact(); jest.resetModules(); act = require('internal-test-utils').act; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; Stream = require('stream'); React = require('react'); use = React.use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server.node'); ReactServerDOMClient = require('react-server-dom-webpack/client'); ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; static getDerivedStateFromError(error) { return { hasError: true, error, }; } render() { if (this.state.hasError) { return this.props.fallback(this.state.error); } return this.props.children; } }; }); async function readInto( container: Document | HTMLElement, stream: ReadableStream, ) { const reader = stream.getReader(); const decoder = new TextDecoder(); let content = ''; while (true) { const {done, value} = await reader.read(); if (done) { content += decoder.decode(); break; } content += decoder.decode(value, {stream: true}); } if (container.nodeType === 9 /* DOCUMENT */) { const doc = new JSDOM(content).window.document; container.documentElement.innerHTML = doc.documentElement.innerHTML; while (container.documentElement.attributes.length > 0) { container.documentElement.removeAttribute( container.documentElement.attributes[0].name, ); } const attrs = doc.documentElement.attributes; for (let i = 0; i < attrs.length; i++) { container.documentElement.setAttribute(attrs[i].name, attrs[i].value); } } else { container.innerHTML = content; } } function getTestStream() { const writable = new Stream.PassThrough(); const readable = new ReadableStream({ start(controller) { writable.on('data', chunk => { controller.enqueue(chunk); }); writable.on('end', () => { controller.close(); }); }, }); return { readable, writable, }; } function createUnclosingStream( stream: ReadableStream, ): ReadableStream { const reader = stream.getReader(); const s = new ReadableStream({ async pull(controller) { const {done, value} = await reader.read(); if (!done) { controller.enqueue(value); } }, }); return s; } const theInfinitePromise = new Promise(() => {}); function InfiniteSuspend() { throw theInfinitePromise; } function getMeaningfulChildren(element) { const children = []; let node = element.firstChild; while (node) { if (node.nodeType === 1) { if ( // some tags are ambiguous and might be hidden because they look like non-meaningful children // so we have a global override where if this data attribute is included we also include the node node.hasAttribute('data-meaningful') || (node.tagName === 'SCRIPT' && node.hasAttribute('src') && node.hasAttribute('async')) || (node.tagName !== 'SCRIPT' && node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && !node.hasAttribute('aria-hidden') && // Ignore the render blocking expect (node.getAttribute('rel') !== 'expect' || node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; for (let i = 0; i < attributes.length; i++) { if ( attributes[i].name === 'id' && attributes[i].value.includes(':') ) { // We assume this is a React added ID that's a non-visual implementation detail. continue; } props[attributes[i].name] = attributes[i].value; } props.children = getMeaningfulChildren(node); children.push(React.createElement(node.tagName.toLowerCase(), props)); } } else if (node.nodeType === 3) { children.push(node.data); } node = node.nextSibling; } return children.length === 0 ? undefined : children.length === 1 ? children[0] : children; } it('should resolve HTML using Node streams', async () => { function Text({children}) { return {children}; } function HTML() { return (
hello world
); } function App() { const model = { html: , }; return model; } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const model = await response; expect(model).toEqual({ html: (
hello world
), }); }); it('should resolve the root', async () => { // Model function Text({children}) { return {children}; } function HTML() { return (
hello world
); } function RootModel() { return { html: , }; } // View function Message({response}) { return
{use(response).html}
; } function App({response}) { return ( Loading...}> ); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe( '
helloworld
', ); }); it('should not get confused by $', async () => { // Model function RootModel() { return {text: '$1'}; } // View function Message({response}) { return

{use(response).text}

; } function App({response}) { return ( Loading...}> ); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

$1

'); }); it('should not get confused by @', async () => { // Model function RootModel() { return {text: '@div'}; } // View function Message({response}) { return

{use(response).text}

; } function App({response}) { return ( Loading...}> ); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

@div

'); }); it('should be able to esm compat test module references', async () => { const ESMCompatModule = { __esModule: true, default: function ({greeting}) { return greeting + ' World'; }, hi: 'Hello', }; function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } function interopWebpack(obj) { // Basically what Webpack's ESM interop feature testing does. if (typeof obj === 'object' && obj.__esModule) { return obj; } return Object.assign({default: obj}, obj); } const {default: Component, hi} = interopWebpack( clientExports(ESMCompatModule), ); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Hello World

'); }); it('should be able to render a named component export', async () => { const Module = { Component: function ({greeting}) { return greeting + ' World'; }, }; function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } const {Component} = clientExports(Module); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Hello World

'); }); it('should be able to render a module split named component export', async () => { const Module = { // This gets split into a separate module from the original one. split: function ({greeting}) { return greeting + ' World'; }, }; function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } const {split: Component} = clientExports(Module); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Hello World

'); }); it('should unwrap async module references', async () => { const AsyncModule = Promise.resolve(function AsyncModule({text}) { return 'Async: ' + text; }); const AsyncModule2 = Promise.resolve({ exportName: 'Module', }); function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } const AsyncModuleRef = await clientExports(AsyncModule); const AsyncModuleRef2 = await clientExports(AsyncModule2); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Async: Module

'); }); it('should unwrap async module references using use', async () => { const AsyncModule = Promise.resolve('Async Text'); function Print({response}) { return use(response); } function App({response}) { return ( Loading...}> ); } const AsyncModuleRef = clientExports(AsyncModule); function ServerComponent() { const text = FlightReact.use(AsyncModuleRef); return

{text}

; } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Async Text

'); }); it('should unwrap async ESM module references', async () => { const AsyncModule = Promise.resolve(function AsyncModule({text}) { return 'Async: ' + text; }); const AsyncModule2 = Promise.resolve({ exportName: 'Module', }); function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } const AsyncModuleRef = await clientExportsESM(AsyncModule); const AsyncModuleRef2 = await clientExportsESM(AsyncModule2); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

Async: Module

'); }); it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => { const AsyncModule = Promise.resolve(function AsyncModule() { return 'This should not be rendered'; }); function Print({response}) { return

{use(response)}

; } function App({response}) { return ( (

{__DEV__ ? error.message + ' + ' : null} {error.digest}

)}> Loading...}>
); } const AsyncModuleRef = await clientExportsESM(AsyncModule, { forceClientModuleProxy: true, }); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, { onError(error) { return __DEV__ ? 'a dev digest' : `digest(${error.message})`; }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`; expect(container.innerHTML).toBe( __DEV__ ? `

${errorMessage} + a dev digest

` : `

digest(${errorMessage})

`, ); }); it('should be able to import a name called "then"', async () => { const thenExports = { then: function then() { return 'and then'; }, }; function Print({response}) { return

{use(response)}

; } function App({response}) { return ( Loading...}> ); } const ThenRef = clientExports(thenExports).then; const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

and then

'); }); it('throws when accessing a member below the client exports', () => { const ClientModule = clientExports({ Component: {deep: 'thing'}, }); function dotting() { return ClientModule.Component.deep; } expect(dotting).toThrowError( 'Cannot access Component.deep on the server. ' + 'You cannot dot into a client module from a server component. ' + 'You can only pass the imported name through.', ); }); it('throws when await a client module prop of client exports', async () => { const ClientModule = clientExports({ Component: {deep: 'thing'}, }); async function awaitExport() { const mod = await ClientModule; return await Promise.resolve(mod.Component); } await expect(awaitExport()).rejects.toThrowError( `Cannot await or return from a thenable. ` + `You cannot await a client module from a server component.`, ); }); it('throws when accessing a symbol prop from client exports', () => { const symbol = Symbol('test'); const ClientModule = clientExports({ Component: {deep: 'thing'}, }); function read() { return ClientModule[symbol]; } expect(read).toThrowError( 'Cannot read Symbol exports. ' + 'Only named exports are supported on a client module imported on the server.', ); }); it('does not throw when toString:ing client exports', () => { const ClientModule = clientExports({ Component: {deep: 'thing'}, }); expect(Object.prototype.toString.call(ClientModule)).toBe( '[object Object]', ); expect(Object.prototype.toString.call(ClientModule.Component)).toBe( '[object Function]', ); }); it('does not throw when React inspects any deep props', () => { const ClientModule = clientExports({ Component: function () {}, }); ; }); it('does not throw when accessing a Context.Provider from client exports', () => { const Context = React.createContext(); const ClientModule = clientExports({ Context, }); function dotting() { return ClientModule.Context.Provider; } expect(dotting).not.toThrowError(); }); it('can render a client Context.Provider from a server component', async () => { // Create a context in a client module const TestContext = React.createContext('default'); const ClientModule = clientExports({ TestContext, }); // Client component that reads context function ClientConsumer() { const value = React.useContext(TestContext); return {value}; } const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer}); function Print({response}) { return use(response); } function App({response}) { return ( Loading...}> ); } // Server component that provides context function ServerApp() { return (
); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('
from-server
'); }); it('should progressively reveal server components', async () => { let reportedErrors = []; // Client Components function MyErrorBoundary({children}) { return ( (

{__DEV__ ? e.message + ' + ' : null} {e.digest}

)}> {children}
); } // Model function Text({children}) { return children; } function makeDelayedText() { let _resolve, _reject; let promise = new Promise((resolve, reject) => { _resolve = () => { promise = null; resolve(); }; _reject = e => { promise = null; reject(e); }; }); async function DelayedText({children}) { await promise; return {children}; } return [DelayedText, _resolve, _reject]; } const [Friends, resolveFriends] = makeDelayedText(); const [Name, resolveName] = makeDelayedText(); const [Posts, resolvePosts] = makeDelayedText(); const [Photos, resolvePhotos] = makeDelayedText(); const [Games, , rejectGames] = makeDelayedText(); // View function ProfileDetails({avatar}) { return (
:name: {avatar}
); } function ProfileSidebar({friends}) { return (
:photos: {friends}
); } function ProfilePosts({posts}) { return
{posts}
; } function ProfileGames({games}) { return
{games}
; } const MyErrorBoundaryClient = clientExports(MyErrorBoundary); function ProfileContent() { return ( <> :avatar:} /> (loading sidebar)

}> :friends:} />
(loading posts)

}> :posts:} />
(loading games)

}> :games:} />
); } const model = { rootContent: , }; function ProfilePage({response}) { return use(response).rootContent; } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(model, webpackMap, { onError(x) { reportedErrors.push(x); return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; }, }), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( (loading)

}>
, ); }); expect(container.innerHTML).toBe('

(loading)

'); // 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. await act(() => { jest.advanceTimersByTime(500); }); expect(container.innerHTML).toBe( '
:name::avatar:
' + '

(loading sidebar)

' + '

(loading posts)

' + '

(loading games)

', ); expect(reportedErrors).toEqual([]); const theError = new Error('Game over'); // Let's *fail* loading games. await serverAct(async () => { await act(async () => { await rejectGames(theError); await 'the inner async function'; }); }); const expectedGamesValue = __DEV__ ? '

Game over + a dev digest

' : '

digest("Game over")

'; expect(container.innerHTML).toBe( '
:name::avatar:
' + '

(loading sidebar)

' + '

(loading posts)

' + expectedGamesValue, ); expect(reportedErrors).toEqual([theError]); reportedErrors = []; // We can now show the sidebar. await serverAct(async () => { await act(async () => { await resolvePhotos(); await 'the inner async function'; }); }); expect(container.innerHTML).toBe( '
:name::avatar:
' + '
:photos::friends:
' + '

(loading posts)

' + expectedGamesValue, ); // Show everything. await serverAct(async () => { await act(async () => { await resolvePosts(); await 'the inner async function'; }); }); expect(container.innerHTML).toBe( '
:name::avatar:
' + '
:photos::friends:
' + '
:posts:
' + expectedGamesValue, ); expect(reportedErrors).toEqual([]); }); it('should handle streaming async server components', async () => { const reportedErrors = []; const Row = async ({current, next}) => { const chunk = await next; if (chunk.done) { return chunk.value; } return ( ); }; function createResolvablePromise() { let _resolve, _reject; const promise = new Promise((resolve, reject) => { _resolve = resolve; _reject = reject; }); return {promise, resolve: _resolve, reject: _reject}; } function createSuspendedChunk(initialValue) { const {promise, resolve, reject} = createResolvablePromise(); return { row: ( ), resolve, reject, }; } function makeDelayedText() { const {promise, resolve, reject} = createResolvablePromise(); async function DelayedText() { const data = await promise; return
{data}
; } return [DelayedText, resolve, reject]; } const [Posts, resolvePostsData] = makeDelayedText(); const [Photos, resolvePhotosData] = makeDelayedText(); const suspendedChunk = createSuspendedChunk(

loading

); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( suspendedChunk.row, webpackMap, { onError(error) { reportedErrors.push(error); }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function ClientRoot() { return use(response); } await act(() => { root.render(); }); expect(container.innerHTML).toBe('

loading

'); const donePromise = createResolvablePromise(); const value = ( loading posts and photos

}>
); await serverAct(async () => { await act(async () => { suspendedChunk.resolve({value, done: false, next: donePromise.promise}); donePromise.resolve({value, done: true}); }); }); expect(container.innerHTML).toBe('

loading posts and photos

'); await serverAct(async () => { await act(async () => { await resolvePostsData('posts'); await resolvePhotosData('photos'); }); }); expect(container.innerHTML).toBe('
posts
photos
'); expect(reportedErrors).toEqual([]); }); it('should preserve state of client components on refetch', async () => { // Client function Page({response}) { return use(response); } function Input() { return ; } const InputClient = clientExports(Input); // Server function App({color}) { // Verify both DOM and Client children. return (
); } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); const stream1 = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(stream1.writable); const response1 = ReactServerDOMClient.createFromReadableStream( stream1.readable, ); await act(() => { root.render( (loading)

}>
, ); }); expect(container.children.length).toBe(1); expect(container.children[0].tagName).toBe('DIV'); expect(container.children[0].style.color).toBe('red'); // Change the DOM state for both inputs. const inputA = container.children[0].children[0]; expect(inputA.tagName).toBe('INPUT'); inputA.value = 'hello'; const inputB = container.children[0].children[1]; expect(inputB.tagName).toBe('INPUT'); inputB.value = 'goodbye'; const stream2 = getTestStream(); const {pipe: pipe2} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe2(stream2.writable); const response2 = ReactServerDOMClient.createFromReadableStream( stream2.readable, ); await act(() => { root.render( (loading)

}>
, ); }); expect(container.children.length).toBe(1); expect(container.children[0].tagName).toBe('DIV'); expect(container.children[0].style.color).toBe('blue'); // Verify we didn't destroy the DOM for either input. expect(inputA === container.children[0].children[0]).toBe(true); expect(inputA.tagName).toBe('INPUT'); expect(inputA.value).toBe('hello'); expect(inputB === container.children[0].children[1]).toBe(true); expect(inputB.tagName).toBe('INPUT'); expect(inputB.value).toBe('goodbye'); }); it('should be able to complete after aborting and throw the reason client-side', async () => { const reportedErrors = []; const {writable, readable} = getTestStream(); const {pipe, abort} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(
, webpackMap, { onError(x) { reportedErrors.push(x); const message = typeof x === 'string' ? x : x.message; return __DEV__ ? 'a dev digest' : `digest("${message}")`; }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App({res}) { return use(res); } await act(() => { root.render( (

{__DEV__ ? e.message + ' + ' : null} {e.digest}

)}> (loading)

}>
, ); }); expect(container.innerHTML).toBe('

(loading)

'); await act(() => { abort('for reasons'); }); if (__DEV__) { expect(container.innerHTML).toBe('

for reasons + a dev digest

'); } else { expect(container.innerHTML).toBe('

digest("for reasons")

'); } expect(reportedErrors).toEqual(['for reasons']); }); it('should be able to recover from a direct reference erroring client-side', async () => { const reportedErrors = []; const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); const ClientReference = clientModuleError(new Error('module init error')); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(
, webpackMap, { onError(x) { reportedErrors.push(x); }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App({res}) { return use(res); } await act(() => { root.render(

{e.message}

}> (loading)

}>
, ); }); expect(container.innerHTML).toBe('

module init error

'); expect(reportedErrors).toEqual([]); }); it('should be able to recover from a direct reference erroring client-side async', async () => { const reportedErrors = []; const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); let rejectPromise; const ClientReference = await clientExports( new Promise((resolve, reject) => { rejectPromise = reject; }), ); const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(
, webpackMap, { onError(x) { reportedErrors.push(x); }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App({res}) { return use(res); } await act(() => { root.render(

{e.message}

}> (loading)

}>
, ); }); expect(container.innerHTML).toBe('

(loading)

'); await act(() => { rejectPromise(new Error('async module init error')); }); expect(container.innerHTML).toBe('

async module init error

'); expect(reportedErrors).toEqual([]); }); it('should not retain stale error reason after reentrant module chunk initialization', async () => { function MyComponent() { return
hello from client component
; } const ClientComponent = clientExports(MyComponent); let resolveAsyncComponent; async function AsyncComponent() { await new Promise(r => { resolveAsyncComponent = r; }); return null; } function ServerComponent() { return ( <> ); } const {writable: flightWritable, readable: flightReadable} = getTestStream(); const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(flightWritable); let response = null; function getResponse() { if (response === null) { response = ReactServerDOMClient.createFromReadableStream(flightReadable); } return response; } // Simulate a module that calls captureOwnerStack() during evaluation. // In Fizz SSR, this causes a reentrant readChunk on the same module chunk. // The reentrant require throws a TDZ error. let evaluatingModuleId = null; const origRequire = global.__webpack_require__; global.__webpack_require__ = function (id) { if (id === evaluatingModuleId) { throw new ReferenceError( "Cannot access 'MyComponent' before initialization", ); } const result = origRequire(id); if (result === MyComponent) { evaluatingModuleId = id; if (__DEV__) { React.captureOwnerStack(); } evaluatingModuleId = null; } return result; }; function App() { return use(getResponse()); } await serverAct(async () => { ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable); }); global.__webpack_require__ = origRequire; // Resolve the async component so the Flight stream closes after the client // module chunk was initialized. await serverAct(async () => { resolveAsyncComponent(); }); const container = document.createElement('div'); await readInto(container, fizzReadable); expect(container.innerHTML).toContain('hello from client component'); }); it('should be able to recover from a direct reference erroring server-side', async () => { const reportedErrors = []; const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); // We simulate a bug in the Webpack bundler which causes an error on the server. for (const id in webpackMap) { Object.defineProperty(webpackMap, id, { get: () => { throw new Error('bug in the bundler'); }, }); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(
, webpackMap, { onError(x) { reportedErrors.push(x.message); return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App({res}) { return use(res); } await act(() => { root.render( (

{__DEV__ ? e.message + ' + ' : null} {e.digest}

)}> (loading)

}>
, ); }); if (__DEV__) { expect(container.innerHTML).toBe( '

bug in the bundler + a dev digest

', ); } else { expect(container.innerHTML).toBe('

digest("bug in the bundler")

'); } expect(reportedErrors).toEqual(['bug in the bundler']); }); it('should pass a Promise through props and be able use() it on the client', async () => { async function getData() { return 'async hello'; } function Component({data}) { const text = use(data); return

{text}

; } const ClientComponent = clientExports(Component); function ServerComponent() { const data = getData(); // no await here return ; } function Print({response}) { return use(response); } function App({response}) { return ( Loading...}> ); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('

async hello

'); }); it('should throw on the client if a passed promise eventually rejects', async () => { const reportedErrors = []; const theError = new Error('Server throw'); async function getData() { throw theError; } function Component({data}) { const text = use(data); return

{text}

; } const ClientComponent = clientExports(Component); function ServerComponent() { const data = getData(); // no await here return ; } function Await({response}) { return use(response); } function App({response}) { return ( Loading...}> (

{__DEV__ ? e.message + ' + ' : null} {e.digest}

)}>
); } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, { onError(x) { reportedErrors.push(x); return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; }, }, ), ); pipe(writable); const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe( __DEV__ ? '

Server throw + a dev digest

' : '

digest("Server throw")

', ); expect(reportedErrors).toEqual([theError]); }); it('should support float methods when rendering in Fiber', async () => { function Component() { return

hello world

; } const ClientComponent = clientExports(Component); async function ServerComponent() { FlightReactDOM.prefetchDNS('d before'); FlightReactDOM.preconnect('c before'); FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); FlightReactDOM.preload('l before', {as: 'style'}); FlightReactDOM.preloadModule('lm before'); FlightReactDOM.preloadModule('lm2 before', { crossOrigin: 'anonymous', fetchPriority: 'low', }); FlightReactDOM.preinit('i before', {as: 'script'}); FlightReactDOM.preinitModule('m before'); FlightReactDOM.preinitModule('m2 before', { crossOrigin: 'anonymous', fetchPriority: 'high', }); await 1; FlightReactDOM.prefetchDNS('d after'); FlightReactDOM.preconnect('c after'); FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); FlightReactDOM.preload('l after', {as: 'style'}); FlightReactDOM.preloadModule('lm after'); FlightReactDOM.preloadModule('lm2 after', { crossOrigin: 'anonymous', fetchPriority: 'low', }); FlightReactDOM.preinit('i after', {as: 'script'}); FlightReactDOM.preinitModule('m after'); FlightReactDOM.preinitModule('m2 after', { crossOrigin: 'anonymous', fetchPriority: 'high', }); return ; } const {writable, readable} = getTestStream(); const {pipe} = await serverAct(() => ReactServerDOMServer.renderToPipeableStream( , webpackMap, ), ); pipe(writable); let response = null; function getResponse() { if (response === null) { response = ReactServerDOMClient.createFromReadableStream(readable); } return response; } function App() { return getResponse(); } // We pause to allow the float call after the await point to process before the // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing // because the module graphs are not different and the HostDispatcher is shared. // In a real environment the Fiber and Flight code would each have their own independent // dispatcher. // @TODO consider what happens when Server-Components-On-The-Client exist. we probably // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime // but we need to make sure that actually makes sense await 1; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(getMeaningfulChildren(document)).toEqual(