/*** 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* @jest-environment node*/'use strict';
let Stream;
let React;
let ReactDOMFizzServer;
let Suspense;
describe('ReactDOMFizzServerNode', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
});function getTestWritable() {
const writable = new Stream.PassThrough();
writable.setEncoding('utf8');
const output = {result: '', error: undefined};
writable.on('data', chunk => {
output.result += chunk;
});writable.on('error', error => {
output.error = error;
});const completed = new Promise(resolve => {
writable.on('finish', () => {
resolve();
});writable.on('error', () => {
resolve();
});});return {writable, completed, output};
}const theError = new Error('This is an error');
function Throw() {
throw theError;
}const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}it('should call renderToPipeableStream', () => {
const {writable, output} = getTestWritable();
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>hello world</div>,
);pipe(writable);jest.runAllTimers();
expect(output.result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
});it('should emit DOCTYPE at the root of the document', () => {const {writable, output} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html><body>hello world</body></html>,);pipe(writable);jest.runAllTimers();
if (gate(flags => flags.enableFloat)) {
// with Float, we emit empty heads if they are elided when rendering <html>expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,);} else {expect(output.result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><body>hello world</body></html>"`,);}});it('should emit bootstrap script src at the end', () => {const {writable, output} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>hello world</div>,{bootstrapScriptContent: 'INIT();',bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
},);pipe(writable);jest.runAllTimers();
expect(output.result).toMatchInlineSnapshot(
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);});it('should start writing after pipe', () => {const {writable, output} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>hello world</div>,);jest.runAllTimers();
// First we write our header.
output.result +=
'<!doctype html><html><head><title>test</title><head><body>';// Then React starts writing.
pipe(writable);expect(output.result).toMatchInlineSnapshot(
`"<!doctype html><html><head><title>test</title><head><body><div>hello world</div>"`,);});it('emits all HTML as one unit if we wait until the end to start', async () => {let hasLoaded = false;let resolve;const promise = new Promise(r => (resolve = r));function Wait() {if (!hasLoaded) {throw promise;}return 'Done';}let isCompleteCalls = 0;const {writable, output} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback="Loading"><Wait /></Suspense></div>,{onAllReady() {isCompleteCalls++;
},},);await jest.runAllTimers();
expect(output.result).toBe('');
expect(isCompleteCalls).toBe(0);
// Resolve the loading.
hasLoaded = true;await resolve();await jest.runAllTimers();
expect(output.result).toBe('');
expect(isCompleteCalls).toBe(1);
// First we write our header.
output.result +=
'<!doctype html><html><head><title>test</title><head><body>';// Then React starts writing.
pipe(writable);expect(output.result).toMatchInlineSnapshot(
`"<!doctype html><html><head><title>test</title><head><body><div><!--$-->Done<!-- --><!--/$--></div>"`,
);});it('should error the stream when an error is thrown at the root', async () => {const reportedErrors = [];
const reportedShellErrors = [];
const {writable, output, completed} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div><Throw /></div>,{onError(x) {reportedErrors.push(x);
},onShellError(x) {reportedShellErrors.push(x);
},},);// The stream is errored once we start writing.
pipe(writable);await completed;expect(output.error).toBe(theError);
expect(output.result).toBe('');
// This type of error is reported to the error callback too.
expect(reportedErrors).toEqual([theError]);
expect(reportedShellErrors).toEqual([theError]);
});it('should error the stream when an error is thrown inside a fallback', async () => {const reportedErrors = [];
const reportedShellErrors = [];
const {writable, output, completed} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback={<Throw />}><InfiniteSuspend /></Suspense></div>,{onError(x) {reportedErrors.push(x.message);
},onShellError(x) {reportedShellErrors.push(x);
},},);pipe(writable);await completed;expect(output.error).toBe(theError);
expect(output.result).toBe('');
expect(reportedErrors).toEqual([
theError.message,'The destination stream errored while writing data.',]);
expect(reportedShellErrors).toEqual([theError]);
});it('should not error the stream when an error is thrown inside suspense boundary', async () => {const reportedErrors = [];
const reportedShellErrors = [];
const {writable, output, completed} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback={<div>Loading</div>}><Throw /></Suspense></div>,{onError(x) {reportedErrors.push(x);
},onShellError(x) {reportedShellErrors.push(x);
},},);pipe(writable);await completed;expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
// While no error is reported to the stream, the error is reported to the callback.
expect(reportedErrors).toEqual([theError]);
expect(reportedShellErrors).toEqual([]);
});it('should not attempt to render the fallback if the main content completes first', async () => {const {writable, output, completed} = getTestWritable();let renderedFallback = false;function Fallback() {renderedFallback = true;return 'Loading...';
}function Content() {return 'Hi';}const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<Suspense fallback={<Fallback />}><Content /></Suspense>,);pipe(writable);await completed;expect(output.result).toContain('Hi');
expect(output.result).not.toContain('Loading');
expect(renderedFallback).toBe(false);
});it('should be able to complete by aborting even if the promise never resolves', async () => {let isCompleteCalls = 0;const errors = [];
const {writable, output, completed} = getTestWritable();const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback={<div>Loading</div>}><InfiniteSuspend /></Suspense></div>,{onError(x) {errors.push(x.message);
},onAllReady() {isCompleteCalls++;
},},);pipe(writable);jest.runAllTimers();
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(0);
abort(new Error('uh oh'));await completed;expect(errors).toEqual(['uh oh']);
expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(1);
});it('should fail the shell if you abort before work has begun', async () => {let isCompleteCalls = 0;const errors = [];
const shellErrors = [];
const {writable, output, completed} = getTestWritable();const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback={<div>Loading</div>}><InfiniteSuspend /></Suspense></div>,{onError(x) {errors.push(x.message);
},onShellError(x) {shellErrors.push(x.message);
},onAllReady() {isCompleteCalls++;
},},);pipe(writable);// Currently we delay work so if we abort, we abort the remaining CPU// work as well.
// Abort before running the timers that perform the workconst theReason = new Error('uh oh');abort(theReason);jest.runAllTimers();
await completed;expect(errors).toEqual(['uh oh']);
expect(shellErrors).toEqual(['uh oh']);
expect(output.error).toBe(theReason);
expect(output.result).toBe('');
expect(isCompleteCalls).toBe(0);
});it('should be able to complete by abort when the fallback is also suspended', async () => {let isCompleteCalls = 0;const errors = [];
const {writable, output, completed} = getTestWritable();const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback="Loading"><Suspense fallback={<InfiniteSuspend />}><InfiniteSuspend /></Suspense></Suspense></div>,{onError(x) {errors.push(x.message);
},onAllReady() {isCompleteCalls++;
},},);pipe(writable);jest.runAllTimers();
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(0);
abort();await completed;expect(errors).toEqual([
// There are two boundaries that abort'The render was aborted by the server without a reason.','The render was aborted by the server without a reason.',]);
expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
expect(isCompleteCalls).toBe(1);
});it('should be able to get context value when promise resolves', async () => {class DelayClient {get() {if (this.resolved) return this.resolved;
if (this.pending) return this.pending;
return (this.pending = new Promise(resolve => {
setTimeout(() => {delete this.pending;
this.resolved = 'OK';
resolve();}, 500);}));}}const DelayContext = React.createContext(undefined);
const Component = () => {const client = React.useContext(DelayContext);
if (!client) {return 'context not found.';
}const result = client.get();
if (typeof result === 'string') {return result;}throw result;};const client = new DelayClient();const {writable, output, completed} = getTestWritable();ReactDOMFizzServer.renderToPipeableStream(
<DelayContext.Provider value={client}>
<Suspense fallback="loading"><Component /></Suspense></DelayContext.Provider>,
).pipe(writable);
jest.runAllTimers();
expect(output.error).toBe(undefined);
expect(output.result).toContain('loading');
await completed;expect(output.error).toBe(undefined);
expect(output.result).not.toContain('context never found');
expect(output.result).toContain('OK');
});it('should be able to get context value when calls renderToPipeableStream twice at the same time', async () => {class DelayClient {get() {if (this.resolved) return this.resolved;
if (this.pending) return this.pending;
return (this.pending = new Promise(resolve => {
setTimeout(() => {delete this.pending;
this.resolved = 'OK';
resolve();}, 500);}));}}const DelayContext = React.createContext(undefined);
const Component = () => {const client = React.useContext(DelayContext);
if (!client) {return 'context never found';}const result = client.get();
if (typeof result === 'string') {return result;}throw result;};const client0 = new DelayClient();const {writable: writable0,output: output0,completed: completed0,} = getTestWritable();ReactDOMFizzServer.renderToPipeableStream(
<DelayContext.Provider value={client0}>
<Suspense fallback="loading"><Component /></Suspense></DelayContext.Provider>,
).pipe(writable0);
const client1 = new DelayClient();const {writable: writable1,output: output1,completed: completed1,} = getTestWritable();ReactDOMFizzServer.renderToPipeableStream(
<DelayContext.Provider value={client1}>
<Suspense fallback="loading"><Component /></Suspense></DelayContext.Provider>,
).pipe(writable1);
jest.runAllTimers();
expect(output0.error).toBe(undefined);
expect(output0.result).toContain('loading');
expect(output1.error).toBe(undefined);
expect(output1.result).toContain('loading');
await Promise.all([completed0, completed1]);
expect(output0.error).toBe(undefined);
expect(output0.result).not.toContain('context never found');
expect(output0.result).toContain('OK');
expect(output1.error).toBe(undefined);
expect(output1.result).not.toContain('context never found');
expect(output1.result).toContain('OK');
});it('should be able to pop context after suspending', async () => {class DelayClient {get() {if (this.resolved) return this.resolved;
if (this.pending) return this.pending;
return (this.pending = new Promise(resolve => {
setTimeout(() => {delete this.pending;
this.resolved = 'OK';
resolve();}, 500);}));}}const DelayContext = React.createContext(undefined);
const Component = () => {const client = React.useContext(DelayContext);
if (!client) {return 'context not found.';
}const result = client.get();
if (typeof result === 'string') {return result;}throw result;};const client = new DelayClient();const {writable, output, completed} = getTestWritable();ReactDOMFizzServer.renderToPipeableStream(
<><DelayContext.Provider value={client}>
<Suspense fallback="loading"><Component /></Suspense></DelayContext.Provider>
<DelayContext.Provider value={client}>
<Suspense fallback="loading"><Component /></Suspense></DelayContext.Provider>
</>,).pipe(writable);
jest.runAllTimers();
expect(output.error).toBe(undefined);
expect(output.result).toContain('loading');
await completed;expect(output.error).toBe(undefined);
expect(output.result).not.toContain('context never found');
expect(output.result).toContain('OK');
});it('should not continue rendering after the writable ends unexpectedly', async () => {let hasLoaded = false;let resolve;let isComplete = false;let rendered = false;const promise = new Promise(r => (resolve = r));function Wait() {if (!hasLoaded) {throw promise;}rendered = true;return 'Done';}const errors = [];
const {writable, completed} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div><Suspense fallback={<div>Loading</div>}><Wait /></Suspense></div>,{onError(x) {errors.push(x.message);
},onAllReady() {isComplete = true;},},);pipe(writable);expect(rendered).toBe(false);
expect(isComplete).toBe(false);
writable.end();
await jest.runAllTimers();
hasLoaded = true;resolve();await completed;expect(errors).toEqual([
'The destination stream errored while writing data.',]);
expect(rendered).toBe(false);
expect(isComplete).toBe(true);
});it('should encode multibyte characters correctly without nulls (#24985)', () => {const {writable, output} = getTestWritable();const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>{Array(700).fill('ののの')}</div>,
);pipe(writable);jest.runAllTimers();
expect(output.result.indexOf('\u0000')).toBe(-1);
expect(output.result).toEqual(
'<div>' + Array(700).fill('ののの').join('<!-- -->') + '</div>',
);});});