/**
* 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 (
',
);
});
it('should not get confused by $', async () => {
// Model
function RootModel() {
return {text: '$1'};
}
// View
function Message({response}) {
return
');
});
it('should not get confused by @', async () => {
// Model
function RootModel() {
return {text: '@div'};
}
// View
function Message({response}) {
return
');
});
it('should be able to render a named component export', async () => {
const Module = {
Component: function ({greeting}) {
return greeting + ' World';
},
};
function Print({response}) {
return
');
});
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
');
});
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
)}>
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
');
});
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 (
');
// 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(
'
');
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(
)}>
(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
');
});
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...}>
(
',
);
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(
,
);
expect(getMeaningfulChildren(container)).toEqual(
hello world
);
});
it('should support float methods when rendering in Fizz', 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: flightWritable, readable: flightReadable} =
getTestStream();
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
// In a real environment you would want to call the render during the Fizz render.
// The reason we cannot do this in our test is because we don't actually have two separate
// module graphs and we are contriving the sequencing to work in a way where
// the right HostDispatcher is in scope during the Flight Server Float calls and the
// Flight Client hint dispatches
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
),
);
pipe(flightWritable);
let response = null;
function getResponse() {
if (response === null) {
response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
}
return response;
}
function App() {
return (
{getResponse()}
);
}
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable);
});
await readInto(document, fizzReadable);
expect(getMeaningfulChildren(document)).toEqual(
hello world
,
);
});
it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
function Component() {
return
hello world
;
}
const ClientComponent = clientExports(Component);
async function ServerComponent1() {
FlightReactDOM.preload('before1', {as: 'style'});
await 1;
FlightReactDOM.preload('after1', {as: 'style'});
return ;
}
async function ServerComponent2() {
FlightReactDOM.preload('before2', {as: 'style'});
await 1;
FlightReactDOM.preload('after2', {as: 'style'});
return ;
}
const {writable: flightWritable1, readable: flightReadable1} =
getTestStream();
const {writable: flightWritable2, readable: flightReadable2} =
getTestStream();
ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
).pipe(flightWritable1);
ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
).pipe(flightWritable2);
const responses = new Map();
function getResponse(stream) {
let response = responses.get(stream);
if (!response) {
response = ReactServerDOMClient.createFromReadableStream(stream);
responses.set(stream, response);
}
return response;
}
function App({stream}) {
return (
{getResponse(stream)}
);
}
// pausing to let Flight runtime tick. This is a test only artifact of the fact that
// we aren't operating separate module graphs for flight and fiber. In a real app
// each would have their own dispatcher and there would be no cross dispatching.
await serverAct(() => {});
const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
,
).pipe(fizzWritable1);
ReactDOMFizzServer.renderToPipeableStream(
,
).pipe(fizzWritable2);
});
async function read(stream) {
const decoder = new TextDecoder();
const reader = stream.getReader();
let buffer = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
buffer += decoder.decode();
break;
}
buffer += decoder.decode(value, {stream: true});
}
return buffer;
}
const [content1, content2] = await Promise.all([
read(fizzReadable1),
read(fizzReadable2),
]);
expect(content1).toEqual(
'' +
'' +
(gate(flags => flags.enableFizzBlockingRender)
? ''
: '') +
'' +
'
;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
loading 3...
,
);
});
it('can abort during render in an async tick', async () => {
async function Sibling() {
return
;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
loading 3...
,
);
});
it('can abort during render in a lazy initializer for a component', async () => {
function Sibling() {
return
sibling
;
}
function App() {
return (
loading 1...}>
loading 2...}>
loading 3...}>
);
}
const abortRef = {current: null};
const LazyAbort = React.lazy(() => {
abortRef.current();
return {
then(cb) {
cb({default: 'div'});
},
};
});
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
loading 3...
,
);
});
it('can abort during render in a lazy initializer for an element', async () => {
function Sibling() {
return
sibling
;
}
function App() {
return (
loading 1...}>{lazyAbort}loading 2...}>
loading 3...}>
);
}
const abortRef = {current: null};
const lazyAbort = React.lazy(() => {
abortRef.current();
return {
then(cb) {
cb({default: 'hello world'});
},
};
});
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
loading 3...
,
);
});
it('can abort during a synchronous thenable resolution', async () => {
function Sibling() {
return
sibling
;
}
function App() {
return (
loading 1...}>{thenable}loading 2...}>
loading 3...}>
);
}
const abortRef = {current: null};
const thenable = {
then(cb) {
abortRef.current();
cb(thenable.value);
},
};
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
loading 3...
,
);
});
it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
function App() {
return (
loading 1...}>
loading 2...}>{thenable1}
loading 3...}>{thenable2}
);
}
const abortRef = {current: null};
const thenable1 = {
then(cb) {
cb('hello world');
},
};
const thenable2 = {
then(cb) {
cb('hello world');
},
status: 'fulfilled',
value: 'hello world',
};
function ComponentThatAborts() {
abortRef.current();
return thenable1;
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading 1...
loading 2...
hello world
,
);
});
it('can error synchronously after aborting without an unhandled rejection error', async () => {
function App() {
return (
loading...}>
);
}
const abortRef = {current: null};
async function ComponentThatAborts() {
abortRef.current();
throw new Error('boom');
}
const {writable: flightWritable, readable: flightReadable} =
getTestStream();
await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});
assertConsoleErrorDev([
'Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
function ClientApp() {
return use(response);
}
const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in ',
]);
expect(shellErrors).toEqual([]);
const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
loading...
,
);
});
it('can error synchronously after aborting in a synchronous Component', async () => {
const rejectError = new Error('bam!');
const rejectedPromise = Promise.reject(rejectError);
rejectedPromise.catch(() => {});
rejectedPromise.status = 'rejected';
rejectedPromise.reason = rejectError;
const resolvedValue =
,
);
});
it('rejecting a thenable after an abort before flush should not lead to a frozen readable', async () => {
const ClientComponent = clientExports(function (props: {
promise: Promise,
}) {
return 'hello world';
});
let reject;
const promise = new Promise((_, re) => {
reject = re;
});
function App() {
return (
);
}
const errors = [];
const {writable, readable} = getTestStream();
const {pipe, abort} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(, webpackMap, {
onError(x) {
errors.push(x);
},
}),
);
await serverAct(() => {
abort('STOP');
reject('STOP');
});
pipe(writable);
const reader = readable.getReader();
while (true) {
const {done} = await reader.read();
if (done) {
break;
}
}
expect(errors).toEqual(['STOP']);
// We expect it to get to the end here rather than hang on the reader.
});
});