/** * 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(() => { 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(
hello world
, ); pipe(writable); jest.runAllTimers(); expect(output.result).toMatchInlineSnapshot(`"
hello world
"`); }); it('should emit DOCTYPE at the root of the document', () => { const {writable, output} = getTestWritable(); const {pipe} = ReactDOMFizzServer.renderToPipeableStream( hello world , ); pipe(writable); jest.runAllTimers(); if (gate(flags => flags.enableFloat)) { // with Float, we emit empty heads if they are elided when rendering expect(output.result).toMatchInlineSnapshot( `"hello world"`, ); } else { expect(output.result).toMatchInlineSnapshot( `"hello world"`, ); } }); it('should emit bootstrap script src at the end', () => { const {writable, output} = getTestWritable(); const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
hello world
, { bootstrapScriptContent: 'INIT();', bootstrapScripts: ['init.js'], bootstrapModules: ['init.mjs'], }, ); pipe(writable); jest.runAllTimers(); expect(output.result).toMatchInlineSnapshot( `"
hello world
"`, ); }); it('should start writing after pipe', () => { const {writable, output} = getTestWritable(); const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
hello world
, ); jest.runAllTimers(); // First we write our header. output.result += 'test'; // Then React starts writing. pipe(writable); expect(output.result).toMatchInlineSnapshot( `"test
hello world
"`, ); }); 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(
, { 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 += 'test'; // Then React starts writing. pipe(writable); expect(output.result).toMatchInlineSnapshot( `"test
Done
"`, ); }); 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(
, { 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(
}>
, { 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(
Loading
}> , { 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( }> , ); 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(
Loading
}> , { 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(
Loading
}> , { 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 work const 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(
}>
, { 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( , ).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( , ).pipe(writable0); const client1 = new DelayClient(); const { writable: writable1, output: output1, completed: completed1, } = getTestWritable(); ReactDOMFizzServer.renderToPipeableStream( , ).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( <> , ).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(
Loading
}> , { 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(
{Array(700).fill('ののの')}
, ); pipe(writable); jest.runAllTimers(); expect(output.result.indexOf('\u0000')).toBe(-1); expect(output.result).toEqual( '
' + Array(700).fill('ののの').join('') + '
', ); }); });