1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @emails react-core8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment9 */1011'use strict';12import {13 insertNodesAndExecuteScripts,14 mergeOptions,15 stripExternalRuntimeInNodes,16 getVisibleChildren,17} from '../test-utils/FizzTestUtils';1819let JSDOM;20let Stream;21let Scheduler;22let React;23let ReactDOM;24let ReactDOMClient;25let ReactDOMFizzServer;26let ReactDOMFizzStatic;27let Suspense;28let SuspenseList;2930let assertConsoleErrorDev;31let useSyncExternalStore;32let useSyncExternalStoreWithSelector;33let use;34let useActionState;35let PropTypes;36let textCache;37let writable;38let CSPnonce = null;39let container;40let buffer = '';41let hasErrored = false;42let fatalError = undefined;43let renderOptions;44let waitFor;45let waitForAll;46let assertLog;47let waitForPaint;48let clientAct;49let streamingContainer;5051function normalizeError(msg) {52 // Take the first sentence to make it easier to assert on.53 const idx = msg.indexOf('.');54 if (idx > -1) {55 return msg.slice(0, idx + 1);56 }57 return msg;58}5960describe('ReactDOMFizzServer', () => {61 beforeEach(() => {62 jest.resetModules();63 JSDOM = require('jsdom').JSDOM;6465 const jsdom = new JSDOM(66 '<!DOCTYPE html><html><head></head><body><div id="container">',67 {68 runScripts: 'dangerously',69 },70 );71 // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else72 Object.defineProperty(jsdom.window, 'matchMedia', {73 writable: true,74 value: jest.fn().mockImplementation(query => ({75 matches: query === 'all' || query === '',76 media: query,77 })),78 });79 streamingContainer = null;80 global.window = jsdom.window;81 global.document = global.window.document;82 global.navigator = global.window.navigator;83 global.Node = global.window.Node;84 global.addEventListener = global.window.addEventListener;85 global.MutationObserver = global.window.MutationObserver;86 // The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.87 global.requestAnimationFrame = global.window.requestAnimationFrame = cb =>88 setTimeout(cb);89 container = document.getElementById('container');9091 CSPnonce = null;92 Scheduler = require('scheduler');93 React = require('react');94 ReactDOM = require('react-dom');95 ReactDOMClient = require('react-dom/client');96 ReactDOMFizzServer = require('react-dom/server');97 ReactDOMFizzStatic = require('react-dom/static');98 Stream = require('stream');99 Suspense = React.Suspense;100 use = React.use;101 if (gate(flags => flags.enableSuspenseList)) {102 SuspenseList = React.unstable_SuspenseList;103 }104 PropTypes = require('prop-types');105 if (__VARIANT__) {106 const originalConsoleError = console.error;107 console.error = (error, ...args) => {108 if (109 typeof error !== 'string' ||110 error.indexOf('ReactDOM.useFormState has been renamed') === -1111 ) {112 originalConsoleError(error, ...args);113 }114 };115116 // Remove after API is deleted.117 useActionState = ReactDOM.useFormState;118 } else {119 useActionState = React.useActionState;120 }121122 ({123 assertConsoleErrorDev,124 assertLog,125 act: clientAct,126 waitFor,127 waitForAll,128 waitForPaint,129 } = require('internal-test-utils'));130131 if (gate(flags => flags.source)) {132 // The `with-selector` module composes the main `use-sync-external-store`133 // entrypoint. In the compiled artifacts, this is resolved to the `shim`134 // implementation by our build config, but when running the tests against135 // the source files, we need to tell Jest how to resolve it. Because this136 // is a source module, this mock has no affect on the build tests.137 jest.mock('use-sync-external-store/src/useSyncExternalStore', () =>138 jest.requireActual('react'),139 );140 }141 useSyncExternalStore = React.useSyncExternalStore;142 useSyncExternalStoreWithSelector =143 require('use-sync-external-store/with-selector').useSyncExternalStoreWithSelector;144145 textCache = new Map();146147 buffer = '';148 hasErrored = false;149150 writable = new Stream.PassThrough();151 writable.setEncoding('utf8');152 writable.on('data', chunk => {153 buffer += chunk;154 });155 writable.on('error', error => {156 hasErrored = true;157 fatalError = error;158 });159160 renderOptions = {};161 if (gate(flags => flags.shouldUseFizzExternalRuntime)) {162 renderOptions.unstable_externalRuntimeSrc =163 'react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js';164 }165 });166167 function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {168 const mappedErrows = errorsArr.map(({error, errorInfo}) => {169 const stack = errorInfo && errorInfo.componentStack;170 const digest = error.digest;171 if (stack) {172 return [error.message, digest, normalizeCodeLocInfo(stack)];173 } else if (digest) {174 return [error.message, digest];175 }176 return error.message;177 });178 if (__DEV__) {179 expect(mappedErrows).toEqual(toBeDevArr);180 } else {181 expect(mappedErrows).toEqual(toBeProdArr);182 }183 }184185 function componentStack(components) {186 return components187 .map(component => `\n in ${component} (at **)`)188 .join('');189 }190191 const bodyStartMatch = /<body(?:>| .*?>)/;192 const headStartMatch = /<head(?:>| .*?>)/;193194 async function act(callback) {195 await callback();196 // Await one turn around the event loop.197 // This assumes that we'll flush everything we have so far.198 await new Promise(resolve => {199 setImmediate(resolve);200 });201 if (hasErrored) {202 throw fatalError;203 }204 // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.205 // We also want to execute any scripts that are embedded.206 // We assume that we have now received a proper fragment of HTML.207 let bufferedContent = buffer;208 buffer = '';209210 if (!bufferedContent) {211 jest.runAllTimers();212 return;213 }214215 const bodyMatch = bufferedContent.match(bodyStartMatch);216 const headMatch = bufferedContent.match(headStartMatch);217218 if (streamingContainer === null) {219 // This is the first streamed content. We decide here where to insert it. If we get <html>, <head>, or <body>220 // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the221 // container. This is not really production behavior because you can't correctly stream into a deep div effectively222 // but it's pragmatic for tests.223224 if (225 bufferedContent.startsWith('<head>') ||226 bufferedContent.startsWith('<head ') ||227 bufferedContent.startsWith('<body>') ||228 bufferedContent.startsWith('<body ')229 ) {230 // wrap in doctype to normalize the parsing process231 bufferedContent = '<!DOCTYPE html><html>' + bufferedContent;232 } else if (233 bufferedContent.startsWith('<html>') ||234 bufferedContent.startsWith('<html ')235 ) {236 throw new Error(237 'Recieved <html> without a <!DOCTYPE html> which is almost certainly a bug in React',238 );239 }240241 if (bufferedContent.startsWith('<!DOCTYPE html>')) {242 // we can just use the whole document243 const tempDom = new JSDOM(bufferedContent);244245 // Wipe existing head and body content246 document.head.innerHTML = '';247 document.body.innerHTML = '';248249 // Copy the <html> attributes over250 const tempHtmlNode = tempDom.window.document.documentElement;251 for (let i = 0; i < tempHtmlNode.attributes.length; i++) {252 const attr = tempHtmlNode.attributes[i];253 document.documentElement.setAttribute(attr.name, attr.value);254 }255256 if (headMatch) {257 // We parsed a head open tag. we need to copy head attributes and insert future258 // content into <head>259 streamingContainer = document.head;260 const tempHeadNode = tempDom.window.document.head;261 for (let i = 0; i < tempHeadNode.attributes.length; i++) {262 const attr = tempHeadNode.attributes[i];263 document.head.setAttribute(attr.name, attr.value);264 }265 const source = document.createElement('head');266 source.innerHTML = tempHeadNode.innerHTML;267 await insertNodesAndExecuteScripts(source, document.head, CSPnonce);268 }269270 if (bodyMatch) {271 // We parsed a body open tag. we need to copy head attributes and insert future272 // content into <body>273 streamingContainer = document.body;274 const tempBodyNode = tempDom.window.document.body;275 for (let i = 0; i < tempBodyNode.attributes.length; i++) {276 const attr = tempBodyNode.attributes[i];277 document.body.setAttribute(attr.name, attr.value);278 }279 const source = document.createElement('body');280 source.innerHTML = tempBodyNode.innerHTML;281 await insertNodesAndExecuteScripts(source, document.body, CSPnonce);282 }283284 if (!headMatch && !bodyMatch) {285 throw new Error('expected <head> or <body> after <html>');286 }287 } else {288 // we assume we are streaming into the default container'289 streamingContainer = container;290 const div = document.createElement('div');291 div.innerHTML = bufferedContent;292 await insertNodesAndExecuteScripts(div, container, CSPnonce);293 }294 } else if (streamingContainer === document.head) {295 bufferedContent = '<!DOCTYPE html><html><head>' + bufferedContent;296 const tempDom = new JSDOM(bufferedContent);297298 const tempHeadNode = tempDom.window.document.head;299 const source = document.createElement('head');300 source.innerHTML = tempHeadNode.innerHTML;301 await insertNodesAndExecuteScripts(source, document.head, CSPnonce);302303 if (bodyMatch) {304 streamingContainer = document.body;305306 const tempBodyNode = tempDom.window.document.body;307 for (let i = 0; i < tempBodyNode.attributes.length; i++) {308 const attr = tempBodyNode.attributes[i];309 document.body.setAttribute(attr.name, attr.value);310 }311 const bodySource = document.createElement('body');312 bodySource.innerHTML = tempBodyNode.innerHTML;313 await insertNodesAndExecuteScripts(bodySource, document.body, CSPnonce);314 }315 } else {316 const div = document.createElement('div');317 div.innerHTML = bufferedContent;318 await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce);319 }320 // Let throttled boundaries reveal321 jest.runAllTimers();322 }323324 function resolveText(text) {325 const record = textCache.get(text);326 if (record === undefined) {327 const newRecord = {328 status: 'resolved',329 value: text,330 };331 textCache.set(text, newRecord);332 } else if (record.status === 'pending') {333 const thenable = record.value;334 record.status = 'resolved';335 record.value = text;336 thenable.pings.forEach(t => t());337 }338 }339340 function rejectText(text, error) {341 const record = textCache.get(text);342 if (record === undefined) {343 const newRecord = {344 status: 'rejected',345 value: error,346 };347 textCache.set(text, newRecord);348 } else if (record.status === 'pending') {349 const thenable = record.value;350 record.status = 'rejected';351 record.value = error;352 thenable.pings.forEach(t => t());353 }354 }355356 function readText(text) {357 const record = textCache.get(text);358 if (record !== undefined) {359 switch (record.status) {360 case 'pending':361 throw record.value;362 case 'rejected':363 throw record.value;364 case 'resolved':365 return record.value;366 }367 } else {368 const thenable = {369 pings: [],370 then(resolve) {371 if (newRecord.status === 'pending') {372 thenable.pings.push(resolve);373 } else {374 Promise.resolve().then(() => resolve(newRecord.value));375 }376 },377 };378379 const newRecord = {380 status: 'pending',381 value: thenable,382 };383 textCache.set(text, newRecord);384385 throw thenable;386 }387 }388389 function Text({text}) {390 return text;391 }392393 function AsyncText({text}) {394 return readText(text);395 }396397 function AsyncTextWrapped({as, text}) {398 const As = as;399 return <As>{readText(text)}</As>;400 }401 function renderToPipeableStream(jsx, options) {402 // Merge options with renderOptions, which may contain featureFlag specific behavior403 return ReactDOMFizzServer.renderToPipeableStream(404 jsx,405 mergeOptions(options, renderOptions),406 );407 }408409 // @gate enableBrowserAPI410 it('can opt a component into browser-only rendering', async () => {411 let resolveBrowserText;412 const browserText = new Promise(resolve => {413 resolveBrowserText = resolve;414 });415 let browserReason;416 const initializeReason = jest.fn(() => {417 browserReason = Object.freeze(418 new Error('Only render this content in a browser'),419 );420 return browserReason;421 });422 const browserOnly = ReactDOM.browser(initializeReason);423424 function BrowserOnly() {425 use(browserOnly);426 const text = use(browserText);427 Scheduler.log(text);428 return <span>{text}</span>;429 }430431 function App() {432 return (433 <div>434 <Suspense fallback={<span>Fallback</span>}>435 <BrowserOnly />436 </Suspense>437 </div>438 );439 }440441 const serverErrors = [];442 const browserBailouts = [];443 await act(() => {444 const {pipe} = renderToPipeableStream(<App />, {445 onError(error) {446 serverErrors.push(error);447 },448 onBrowserBailout(error, errorInfo) {449 browserBailouts.push({error, errorInfo});450 },451 });452 pipe(writable);453 });454455 expect(serverErrors).toEqual([]);456 expect(initializeReason).toHaveBeenCalledTimes(1);457 expect(browserBailouts).toHaveLength(1);458 expect(browserBailouts[0].error).toBeInstanceOf(Error);459 expect(browserBailouts[0].error.message).toBe(460 'Browser-only rendering was requested by `browser()`.',461 );462 expect(browserBailouts[0].error.stack).toContain('BrowserOnly');463 expect(browserBailouts[0].error.cause).toBe(browserReason);464 expect(465 normalizeCodeLocInfo(browserBailouts[0].errorInfo.componentStack),466 ).toBe(componentStack(['BrowserOnly', 'Suspense', 'div', 'App']));467 expect(getVisibleChildren(container)).toEqual(468 <div>469 <span>Fallback</span>470 </div>,471 );472 const recoverableErrors = [];473 ReactDOMClient.hydrateRoot(container, <App />, {474 onRecoverableError(error) {475 recoverableErrors.push(error);476 },477 });478 await waitForAll([]);479480 expect(getVisibleChildren(container)).toEqual(481 <div>482 <span>Fallback</span>483 </div>,484 );485486 await clientAct(() => {487 resolveBrowserText('Browser');488 });489 assertLog(['Browser']);490491 expect(recoverableErrors).toEqual([]);492 expect(initializeReason).toHaveBeenCalledTimes(1);493 expect(getVisibleChildren(container)).toEqual(494 <div>495 <span>Browser</span>496 </div>,497 );498 });499500 // @gate enableBrowserAPI501 it('can opt a component into browser-only rendering after streaming the fallback', async () => {502 let resolveServerReady;503 const serverReady = new Promise(resolve => {504 resolveServerReady = resolve;505 });506 const initializeReason = jest.fn(507 () => 'Only render this content in a browser',508 );509510 function BrowserOnly() {511 use(serverReady);512 use(ReactDOM.browser(initializeReason));513 return <span>Browser</span>;514 }515516 function App() {517 return (518 <div>519 <Suspense fallback={<span>Fallback</span>}>520 <BrowserOnly />521 </Suspense>522 </div>523 );524 }525526 const serverErrors = [];527 const browserBailouts = [];528 await act(() => {529 const {pipe} = renderToPipeableStream(<App />, {530 onError(error) {531 serverErrors.push(error);532 },533 onBrowserBailout(error) {534 browserBailouts.push(error);535 },536 });537 pipe(writable);538 });539540 expect(getVisibleChildren(container)).toEqual(541 <div>542 <span>Fallback</span>543 </div>,544 );545546 await act(() => {547 resolveServerReady();548 });549550 expect(serverErrors).toEqual([]);551 expect(initializeReason).toHaveBeenCalledTimes(1);552 expect(browserBailouts).toHaveLength(1);553 expect(browserBailouts[0].message).toBe(554 'Browser-only rendering was requested by `browser()`.',555 );556 expect(browserBailouts[0].stack).toContain('BrowserOnly');557 expect(browserBailouts[0].cause).toBe(558 'Only render this content in a browser',559 );560561 const recoverableErrors = [];562 ReactDOMClient.hydrateRoot(container, <App />, {563 onRecoverableError(error) {564 recoverableErrors.push(error);565 },566 });567 await waitForAll([]);568569 expect(recoverableErrors).toEqual([]);570 expect(initializeReason).toHaveBeenCalledTimes(1);571 expect(getVisibleChildren(container)).toEqual(572 <div>573 <span>Browser</span>574 </div>,575 );576 });577578 // @gate enableBrowserAPI579 it('supports omitted and direct string browser reasons', async () => {580 const directReason = 'Only render this content in a browser';581 const withoutReason = ReactDOM.browser();582 const withDirectReason = ReactDOM.browser(directReason);583584 function WithoutReason() {585 use(withoutReason);586 return <span>Browser</span>;587 }588589 function WithDirectReason() {590 use(withDirectReason);591 return <span>Browser</span>;592 }593594 const serverErrors = [];595 const browserBailouts = [];596 await act(() => {597 const {pipe} = renderToPipeableStream(598 <>599 <Suspense fallback={<span>Fallback A</span>}>600 <WithoutReason />601 </Suspense>602 <Suspense fallback={<span>Fallback B</span>}>603 <WithDirectReason />604 </Suspense>605 </>,606 {607 onError(error) {608 serverErrors.push(error);609 },610 onBrowserBailout(error) {611 browserBailouts.push(error);612 },613 },614 );615 pipe(writable);616 });617618 expect(serverErrors).toEqual([]);619 expect(browserBailouts).toHaveLength(2);620 expect(browserBailouts[0].message).toBe(621 'Browser-only rendering was requested by `browser()`.',622 );623 expect(browserBailouts[0].stack).toContain('WithoutReason');624 expect(625 Object.prototype.hasOwnProperty.call(browserBailouts[0], 'cause'),626 ).toBe(false);627 expect(browserBailouts[1].message).toBe(628 'Browser-only rendering was requested by `browser()`.',629 );630 expect(browserBailouts[1].stack).toContain('WithDirectReason');631 expect(browserBailouts[1].cause).toBe(directReason);632 });633634 // @gate enableBrowserAPI635 it('supports any value returned by a browser reason initializer', async () => {636 const reasonValues = [undefined, null, 42, Symbol('browser reason')];637 const initializeReasons = reasonValues.map(reason => jest.fn(() => reason));638 const browserValues = initializeReasons.map(initializeReason =>639 ReactDOM.browser(initializeReason),640 );641642 function BrowserOnly({browserValue}) {643 use(browserValue);644 return <span>Browser</span>;645 }646647 const serverErrors = [];648 const browserBailouts = [];649 await act(() => {650 const {pipe} = renderToPipeableStream(651 <>652 {browserValues.map((browserValue, index) => (653 <Suspense key={index} fallback={<span>Fallback</span>}>654 <BrowserOnly browserValue={browserValue} />655 </Suspense>656 ))}657 </>,658 {659 onError(error) {660 serverErrors.push(error);661 },662 onBrowserBailout(error) {663 browserBailouts.push(error);664 },665 },666 );667 pipe(writable);668 });669670 expect(serverErrors).toEqual([]);671 expect(browserBailouts).toHaveLength(reasonValues.length);672 initializeReasons.forEach(initializeReason => {673 expect(initializeReason).toHaveBeenCalledTimes(1);674 });675 browserBailouts.forEach((error, index) => {676 expect(error).toBeInstanceOf(Error);677 expect(error.message).toBe(678 'Browser-only rendering was requested by `browser()`.',679 );680 expect(Object.prototype.hasOwnProperty.call(error, 'cause')).toBe(true);681 expect(error.cause).toBe(reasonValues[index]);682 });683 });684685 // @gate enableBrowserAPI686 it('initializes a shared browser reason at each use site', async () => {687 const browserReasons = [];688 const initializeReason = jest.fn(() => {689 const browserReason = {index: browserReasons.length};690 browserReasons.push(browserReason);691 return browserReason;692 });693 const browserValue = ReactDOM.browser(initializeReason);694695 function BrowserOnlyA() {696 use(browserValue);697 return <span>Browser A</span>;698 }699700 function BrowserOnlyB() {701 use(browserValue);702 return <span>Browser B</span>;703 }704705 const browserBailouts = [];706 await act(() => {707 const {pipe} = renderToPipeableStream(708 <>709 <Suspense fallback={<span>Fallback A</span>}>710 <BrowserOnlyA />711 </Suspense>712 <Suspense fallback={<span>Fallback B</span>}>713 <BrowserOnlyB />714 </Suspense>715 </>,716 {717 onBrowserBailout(error) {718 browserBailouts.push(error);719 },720 },721 );722 pipe(writable);723 });724725 expect(initializeReason).toHaveBeenCalledTimes(2);726 expect(browserBailouts).toHaveLength(2);727 expect(browserBailouts[0]).not.toBe(browserBailouts[1]);728 expect(browserBailouts[0].cause).toBe(browserReasons[0]);729 expect(browserBailouts[0].stack).toContain('BrowserOnlyA');730 expect(browserBailouts[1].cause).toBe(browserReasons[1]);731 expect(browserBailouts[1].stack).toContain('BrowserOnlyB');732 });733734 // @gate enableBrowserAPI735 it('uses a fallback if a browser reason initializer throws', async () => {736 const reasonError = new Error('Failed to initialize browser reason');737 const initializeReason = jest.fn(() => {738 throw reasonError;739 });740 const browserValue = ReactDOM.browser(initializeReason);741742 function BrowserOnly() {743 use(browserValue);744 return <span>Browser</span>;745 }746747 const serverErrors = [];748 const browserBailouts = [];749 await act(() => {750 const {pipe} = renderToPipeableStream(751 <Suspense fallback={<span>Fallback</span>}>752 <BrowserOnly />753 </Suspense>,754 {755 onError(error) {756 serverErrors.push(error);757 },758 onBrowserBailout(error) {759 browserBailouts.push(error);760 },761 },762 );763 pipe(writable);764 });765766 expect(initializeReason).toHaveBeenCalledTimes(1);767 expect(serverErrors).toEqual([]);768 expect(browserBailouts).toHaveLength(1);769 expect(browserBailouts[0].cause).toBe(770 'The reason for browser-only rendering could not be determined because ' +771 'its initializer threw.',772 );773 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);774 });775776 // @gate enableBrowserAPI777 it('errors if browser-only content is rendered outside Suspense', async () => {778 const browserReason = 'Only render this content in a browser';779 const browserValue = ReactDOM.browser(browserReason);780781 function BrowserOnly() {782 use(browserValue);783 return <span>Browser</span>;784 }785786 const reportedErrors = [];787 const browserBailouts = [];788 let shellReady = false;789 let shellError;790 await act(() => {791 renderToPipeableStream(<BrowserOnly />, {792 onError(error) {793 reportedErrors.push(error);794 },795 onBrowserBailout(error) {796 browserBailouts.push(error);797 },798 onShellReady() {799 shellReady = true;800 },801 onShellError(error) {802 shellError = error;803 },804 });805 });806807 expect(shellError).toBeInstanceOf(Error);808 expect(shellError.message).toBe(809 'The server render could not complete because client rendering was ' +810 "requested outside a Suspense boundary. See this error's cause for " +811 'additional details.',812 );813 expect(shellError.cause).toBe(browserReason);814 expect(shellError.stack).toContain('BrowserOnly');815 expect(shellError.stack.split('\n')[0]).toBe(816 'Error: ' + shellError.message,817 );818 expect(shellReady).toBe(false);819 expect(reportedErrors).toEqual([shellError]);820 expect(browserBailouts).toEqual([]);821 });822823 // @gate enableBrowserAPI824 it('can abort all pending boundaries into browser-only rendering', async () => {825 const never = new Promise(() => {});826 let isClient = false;827828 function Pending({children}) {829 if (!isClient) {830 use(never);831 }832 return <span>{children}</span>;833 }834835 function App() {836 return (837 <div>838 <span>Shell</span>839 <Suspense fallback={<span>Loading A</span>}>840 <Pending>A</Pending>841 </Suspense>842 <Suspense fallback={<span>Loading B</span>}>843 <Pending>B</Pending>844 </Suspense>845 </div>846 );847 }848849 const serverErrors = [];850 const browserBailouts = [];851 const browserReason = {code: 'render-pending-content-in-browser'};852 const initializeReason = jest.fn(() => browserReason);853 const browserValue = ReactDOM.browser(initializeReason);854 let abort;855 await act(() => {856 const controls = renderToPipeableStream(<App />, {857 onError(error) {858 serverErrors.push(error);859 },860 onBrowserBailout(error) {861 browserBailouts.push(error);862 },863 });864 abort = controls.abort;865 controls.pipe(writable);866 });867868 expect(getVisibleChildren(container)).toEqual(869 <div>870 <span>Shell</span>871 <span>Loading A</span>872 <span>Loading B</span>873 </div>,874 );875876 await act(() => {877 function abortToBrowser() {878 abort(browserValue);879 }880 abortToBrowser();881 });882883 expect(serverErrors).toEqual([]);884 expect(initializeReason).toHaveBeenCalledTimes(1);885 expect(browserBailouts).toHaveLength(2);886 expect(browserBailouts[0]).toBeInstanceOf(Error);887 expect(browserBailouts[0].message).toBe(888 'Browser-only rendering was requested by `browser()`.',889 );890 expect(browserBailouts[0].stack).toContain('abortToBrowser');891 expect(browserBailouts[0].cause).toBe(browserReason);892 expect(browserBailouts[1]).toBe(browserBailouts[0]);893894 isClient = true;895 const recoverableErrors = [];896 ReactDOMClient.hydrateRoot(container, <App />, {897 onRecoverableError(error) {898 recoverableErrors.push(error);899 },900 });901 await waitForAll([]);902903 expect(recoverableErrors).toEqual([]);904 expect(getVisibleChildren(container)).toEqual(905 <div>906 <span>Shell</span>907 <span>A</span>908 <span>B</span>909 </div>,910 );911 });912913 // @gate enableBrowserAPI914 it('errors if aborted with browser() before the shell completes', async () => {915 const never = new Promise(() => {});916 let browserReason;917 const initializeReason = jest.fn(() => {918 browserReason = new Error('Only abort this render on the server');919 return browserReason;920 });921 const browserValue = ReactDOM.browser(initializeReason);922923 function PendingRoot() {924 use(never);925 return <span>Root</span>;926 }927928 const reportedErrors = [];929 const browserBailouts = [];930 let shellReady = false;931 let shellError;932 let abort;933 await act(() => {934 const controls = renderToPipeableStream(<PendingRoot />, {935 onError(error) {936 reportedErrors.push(error);937 },938 onBrowserBailout(error) {939 browserBailouts.push(error);940 },941 onShellReady() {942 shellReady = true;943 },944 onShellError(error) {945 shellError = error;946 },947 });948 abort = controls.abort;949 });950951 await act(() => {952 function abortToBrowser() {953 abort(browserValue);954 }955 abortToBrowser();956 });957958 expect(shellError).toBeInstanceOf(Error);959 expect(initializeReason).toHaveBeenCalledTimes(1);960 expect(shellError.message).toBe(961 'The server render could not complete because client rendering was ' +962 "requested outside a Suspense boundary. See this error's cause for " +963 'additional details.',964 );965 expect(shellError.cause).toBe(browserReason);966 expect(shellError.stack).toContain('abortToBrowser');967 expect(shellReady).toBe(false);968 expect(reportedErrors).toEqual([shellError]);969 expect(browserBailouts).toEqual([]);970 });971972 // @gate enableBrowserAPI973 it('reports nested browser bailouts if aborting fatals the shell', async () => {974 const never = new Promise(() => {});975 const browserReason = 'Abort pending work into browser rendering';976 const browserValue = ReactDOM.browser(browserReason);977978 function Pending() {979 use(never);980 return <span>Pending</span>;981 }982983 const reportedErrors = [];984 const browserBailouts = [];985 let shellError;986 let abort;987 await act(() => {988 const controls = renderToPipeableStream(989 <>990 <Suspense fallback={<span>Fallback</span>}>991 <Pending />992 </Suspense>993 <Pending />994 <Suspense fallback={<span>Fallback</span>}>995 <Pending />996 </Suspense>997 <Pending />998 </>,999 {1000 onError(error) {1001 reportedErrors.push(error);1002 },1003 onBrowserBailout(error) {1004 browserBailouts.push(error);1005 },1006 onShellError(error) {1007 shellError = error;1008 },1009 },1010 );1011 abort = controls.abort;1012 });10131014 await act(() => {1015 abort(browserValue);1016 });10171018 expect(shellError).toBeInstanceOf(Error);1019 expect(shellError.message).toBe(1020 'The server render could not complete because client rendering was ' +1021 "requested outside a Suspense boundary. See this error's cause for " +1022 'additional details.',1023 );1024 expect(shellError.cause).toBe(browserReason);1025 expect(reportedErrors).toHaveLength(2);1026 expect(reportedErrors[0]).toBe(shellError);1027 expect(reportedErrors[1].message).toBe(shellError.message);1028 expect(reportedErrors[1].cause).toBe(browserReason);1029 expect(browserBailouts).toHaveLength(2);1030 expect(browserBailouts[0]).toBe(browserBailouts[1]);1031 expect(browserBailouts[0]).not.toBe(shellError);1032 expect(browserBailouts[0].message).toBe(1033 'Browser-only rendering was requested by `browser()`.',1034 );1035 expect(browserBailouts[0].cause).toBe(browserReason);1036 });10371038 // @gate enableBrowserAPI1039 it('uses a fallback if a browser reason initializer throws during abort', async () => {1040 const never = new Promise(() => {});1041 const reasonError = new Error('Failed to initialize browser reason');1042 const initializeReason = jest.fn(() => {1043 throw reasonError;1044 });1045 const browserValue = ReactDOM.browser(initializeReason);10461047 function PendingRoot() {1048 use(never);1049 return <span>Root</span>;1050 }10511052 const reportedErrors = [];1053 const browserBailouts = [];1054 let shellError;1055 let abort;1056 await act(() => {1057 const controls = renderToPipeableStream(<PendingRoot />, {1058 onError(error) {1059 reportedErrors.push(error);1060 },1061 onBrowserBailout(error) {1062 browserBailouts.push(error);1063 },1064 onShellError(error) {1065 shellError = error;1066 },1067 });1068 abort = controls.abort;1069 });10701071 await act(() => {1072 abort(browserValue);1073 });10741075 expect(initializeReason).toHaveBeenCalledTimes(1);1076 expect(shellError).toBeInstanceOf(Error);1077 expect(shellError.cause).toBe(1078 'The reason for browser-only rendering could not be determined because ' +1079 'its initializer threw.',1080 );1081 expect(reportedErrors).toEqual([shellError]);1082 expect(browserBailouts).toEqual([]);1083 });10841085 // @gate enableBrowserAPI1086 it('reports the browser value if it is thrown instead of passed to use', async () => {1087 const initializeReason = jest.fn(1088 () => new Error('Only render this content in a browser'),1089 );1090 const browserValue = ReactDOM.browser(initializeReason);10911092 function BrowserOnly() {1093 throw browserValue;1094 }10951096 const reportedErrors = [];1097 const browserBailouts = [];1098 await act(() => {1099 const {pipe} = renderToPipeableStream(1100 <Suspense fallback={<span>Fallback</span>}>1101 <BrowserOnly />1102 </Suspense>,1103 {1104 onError(error) {1105 reportedErrors.push(error);1106 },1107 onBrowserBailout(error) {1108 browserBailouts.push(error);1109 },1110 },1111 );1112 pipe(writable);1113 });11141115 expect(reportedErrors).toEqual([browserValue]);1116 expect(browserBailouts).toEqual([]);1117 expect(initializeReason).not.toHaveBeenCalled();1118 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);1119 });11201121 ['', 'BROWSER'].forEach(userDigest => {1122 it(`does not reserve the ${JSON.stringify(1123 userDigest,1124 )} user error digest for browser rendering`, async () => {1125 let isClient = false;1126 const serverError = new Error('Server error');11271128 function ServerError() {1129 if (!isClient) {1130 throw serverError;1131 }1132 return <span>Client</span>;1133 }11341135 function App() {1136 return (1137 <Suspense fallback={<span>Fallback</span>}>1138 <ServerError />1139 </Suspense>1140 );1141 }11421143 const serverErrors = [];1144 await act(() => {1145 const {pipe} = renderToPipeableStream(<App />, {1146 onError(error) {1147 serverErrors.push(error);1148 return userDigest;1149 },1150 });1151 pipe(writable);1152 });11531154 expect(serverErrors).toEqual([serverError]);1155 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);11561157 isClient = true;1158 const recoverableErrors = [];1159 ReactDOMClient.hydrateRoot(container, <App />, {1160 onRecoverableError(error) {1161 recoverableErrors.push(error);1162 },1163 });1164 await waitForAll([]);11651166 expect(recoverableErrors).toHaveLength(1);1167 expect(recoverableErrors[0].digest).toBe(userDigest || undefined);1168 expect(getVisibleChildren(container)).toEqual(<span>Client</span>);1169 });1170 });11711172 it('should asynchronously load a lazy component', async () => {1173 let resolveA;1174 const LazyA = React.lazy(() => {1175 return new Promise(r => {1176 resolveA = r;1177 });1178 });11791180 let resolveB;1181 const LazyB = React.lazy(() => {1182 return new Promise(r => {1183 resolveB = r;1184 });1185 });11861187 class TextWithPunctuation extends React.Component {1188 render() {1189 return <Text text={this.props.text + this.props.punctuation} />;1190 }1191 }11921193 // This tests that default props of the inner element is resolved.1194 TextWithPunctuation.defaultProps = {1195 punctuation: '!',1196 };11971198 await act(() => {1199 const {pipe} = renderToPipeableStream(1200 <div>1201 <div>1202 <Suspense fallback={<Text text="Loading..." />}>1203 <LazyA text="Hello" />1204 </Suspense>1205 </div>1206 <div>1207 <Suspense fallback={<Text text="Loading..." />}>1208 <LazyB text="world" />1209 </Suspense>1210 </div>1211 </div>,1212 );1213 pipe(writable);1214 });12151216 expect(getVisibleChildren(container)).toEqual(1217 <div>1218 <div>Loading...</div>1219 <div>Loading...</div>1220 </div>,1221 );1222 await act(() => {1223 resolveA({default: Text});1224 });1225 expect(getVisibleChildren(container)).toEqual(1226 <div>1227 <div>Hello</div>1228 <div>Loading...</div>1229 </div>,1230 );1231 await act(() => {1232 resolveB({default: TextWithPunctuation});1233 });1234 expect(getVisibleChildren(container)).toEqual(1235 <div>1236 <div>Hello</div>1237 <div>world!</div>1238 </div>,1239 );1240 });12411242 it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {1243 const makeApp = () => {1244 let resolve;1245 const imports = new Promise(r => {1246 resolve = () => r({default: () => <span id="async">async</span>});1247 });1248 const Lazy = React.lazy(() => imports);12491250 const App = () => (1251 <div>1252 <Suspense fallback={<span>Loading...</span>}>1253 <Lazy />1254 <span id="after">after</span>1255 </Suspense>1256 </div>1257 );12581259 return [App, resolve];1260 };12611262 // Server-side1263 const [App, resolve] = makeApp();1264 await act(() => {1265 const {pipe} = renderToPipeableStream(<App />);1266 pipe(writable);1267 });1268 expect(getVisibleChildren(container)).toEqual(1269 <div>1270 <span>Loading...</span>1271 </div>,1272 );1273 await act(() => {1274 resolve();1275 });1276 expect(getVisibleChildren(container)).toEqual(1277 <div>1278 <span id="async">async</span>1279 <span id="after">after</span>1280 </div>,1281 );12821283 // Client-side1284 const [HydrateApp, hydrateResolve] = makeApp();1285 await act(() => {1286 ReactDOMClient.hydrateRoot(container, <HydrateApp />);1287 });12881289 expect(getVisibleChildren(container)).toEqual(1290 <div>1291 <span id="async">async</span>1292 <span id="after">after</span>1293 </div>,1294 );12951296 await act(() => {1297 hydrateResolve();1298 });1299 expect(getVisibleChildren(container)).toEqual(1300 <div>1301 <span id="async">async</span>1302 <span id="after">after</span>1303 </div>,1304 );1305 });13061307 it('should support nonce for bootstrap and runtime scripts', async () => {1308 CSPnonce = 'R4nd0m';1309 try {1310 let resolve;1311 const Lazy = React.lazy(() => {1312 return new Promise(r => {1313 resolve = r;1314 });1315 });13161317 await act(() => {1318 const {pipe} = renderToPipeableStream(1319 <div>1320 <Suspense fallback={<Text text="Loading..." />}>1321 <Lazy text="Hello" />1322 </Suspense>1323 </div>,1324 {1325 nonce: 'R4nd0m',1326 bootstrapScriptContent: 'function noop(){}',1327 bootstrapScripts: [1328 'init.js',1329 {src: 'init2.js', integrity: 'init2hash'},1330 ],1331 bootstrapModules: [1332 'init.mjs',1333 {src: 'init2.mjs', integrity: 'init2hash'},1334 ],1335 },1336 );1337 pipe(writable);1338 });13391340 expect(getVisibleChildren(container)).toEqual([1341 <link1342 rel="preload"1343 fetchpriority="low"1344 href="init.js"1345 as="script"1346 nonce={CSPnonce}1347 />,1348 <link1349 rel="preload"1350 fetchpriority="low"1351 href="init2.js"1352 as="script"1353 nonce={CSPnonce}1354 integrity="init2hash"1355 />,1356 <link1357 rel="modulepreload"1358 fetchpriority="low"1359 href="init.mjs"1360 nonce={CSPnonce}1361 />,1362 <link1363 rel="modulepreload"1364 fetchpriority="low"1365 href="init2.mjs"1366 nonce={CSPnonce}1367 integrity="init2hash"1368 />,1369 <div>Loading...</div>,1370 ]);13711372 // check that there are 6 scripts with a matching nonce:1373 // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules1374 expect(1375 Array.from(container.getElementsByTagName('script')).filter(1376 node => node.getAttribute('nonce') === CSPnonce,1377 ).length,1378 ).toEqual(6);13791380 await act(() => {1381 resolve({default: Text});1382 });1383 expect(getVisibleChildren(container)).toEqual([1384 <link1385 rel="preload"1386 fetchpriority="low"1387 href="init.js"1388 as="script"1389 nonce={CSPnonce}1390 />,1391 <link1392 rel="preload"1393 fetchpriority="low"1394 href="init2.js"1395 as="script"1396 nonce={CSPnonce}1397 integrity="init2hash"1398 />,1399 <link1400 rel="modulepreload"1401 fetchpriority="low"1402 href="init.mjs"1403 nonce={CSPnonce}1404 />,1405 <link1406 rel="modulepreload"1407 fetchpriority="low"1408 href="init2.mjs"1409 nonce={CSPnonce}1410 integrity="init2hash"1411 />,1412 <div>Hello</div>,1413 ]);1414 } finally {1415 CSPnonce = null;1416 }1417 });14181419 it('should not automatically add nonce to rendered scripts', async () => {1420 CSPnonce = 'R4nd0m';1421 try {1422 await act(async () => {1423 const {pipe} = renderToPipeableStream(1424 <html>1425 <body>1426 <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>1427 <script nonce={CSPnonce} src="foo" async={true} />1428 <script src="bar" />1429 <script src="baz" integrity="qux" async={true} />1430 <script type="module" src="quux" async={true} />1431 <script type="module" src="corge" async={true} />1432 <script1433 type="module"1434 src="grault"1435 integrity="garply"1436 async={true}1437 />1438 </body>1439 </html>,1440 {1441 nonce: CSPnonce,1442 },1443 );1444 pipe(writable);1445 });14461447 expect(1448 stripExternalRuntimeInNodes(1449 document.getElementsByTagName('script'),1450 renderOptions.unstable_externalRuntimeSrc,1451 ).map(n => n.outerHTML),1452 ).toEqual([1453 `<script nonce="${CSPnonce}" src="foo" async=""></script>`,1454 `<script src="baz" integrity="qux" async=""></script>`,1455 `<script type="module" src="quux" async=""></script>`,1456 `<script type="module" src="corge" async=""></script>`,1457 `<script type="module" src="grault" integrity="garply" async=""></script>`,1458 `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,1459 `<script src="bar"></script>`,1460 ]);1461 } finally {1462 CSPnonce = null;1463 }1464 });14651466 it('should client render a boundary if a lazy component rejects', async () => {1467 let rejectComponent;1468 const promise = new Promise((resolve, reject) => {1469 rejectComponent = reject;1470 });1471 const LazyComponent = React.lazy(() => {1472 return promise;1473 });14741475 const LazyLazy = React.lazy(async () => {1476 return {1477 default: LazyComponent,1478 };1479 });14801481 function Wrapper({children}) {1482 return children;1483 }1484 const LazyWrapper = React.lazy(() => {1485 return {1486 then(callback) {1487 callback({1488 default: Wrapper,1489 });1490 },1491 };1492 });14931494 function App({isClient}) {1495 return (1496 <div>1497 <Suspense fallback={<Text text="Loading..." />}>1498 <LazyWrapper>1499 {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}1500 </LazyWrapper>1501 </Suspense>1502 </div>1503 );1504 }15051506 let bootstrapped = false;1507 const errors = [];1508 window.__INIT__ = function () {1509 bootstrapped = true;1510 // Attempt to hydrate the content.1511 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1512 onRecoverableError(error, errorInfo) {1513 errors.push({error, errorInfo});1514 },1515 });1516 };15171518 const theError = new Error('Test');1519 const loggedErrors = [];1520 function onError(x, errorInfo) {1521 loggedErrors.push(x);1522 return 'Hash of (' + x.message + ')';1523 }1524 const expectedDigest = onError(theError);1525 loggedErrors.length = 0;15261527 await act(() => {1528 const {pipe} = renderToPipeableStream(<App isClient={false} />, {1529 bootstrapScriptContent: '__INIT__();',1530 onError,1531 });1532 pipe(writable);1533 });15341535 expect(loggedErrors).toEqual([]);1536 expect(bootstrapped).toBe(true);15371538 await waitForAll([]);15391540 // We're still loading because we're waiting for the server to stream more content.1541 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15421543 expect(loggedErrors).toEqual([]);15441545 await act(() => {1546 rejectComponent(theError);1547 });15481549 expect(loggedErrors).toEqual([theError]);15501551 // We haven't ran the client hydration yet.1552 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15531554 // Now we can client render it instead.1555 await waitForAll([]);1556 expectErrors(1557 errors,1558 [1559 [1560 'Switched to client rendering because the server rendering errored:\n\n' +1561 theError.message,1562 expectedDigest,1563 componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),1564 ],1565 ],1566 [1567 [1568 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1569 expectedDigest,1570 ],1571 ],1572 );15731574 // The client rendered HTML is now in place.1575 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);15761577 expect(loggedErrors).toEqual([theError]);1578 });15791580 it('should have special stacks if Suspense fallback', async () => {1581 const infinitePromise = new Promise(() => {});1582 const InfiniteComponent = React.lazy(() => {1583 return infinitePromise;1584 });15851586 function Throw({text}) {1587 throw new Error(text);1588 }15891590 function App() {1591 return (1592 <Suspense fallback="Loading">1593 <div>1594 <Suspense fallback={<Throw text="Bye" />}>1595 <InfiniteComponent text="Hi" />1596 </Suspense>1597 </div>1598 </Suspense>1599 );1600 }16011602 const loggedErrors = [];1603 function onError(x, errorInfo) {1604 loggedErrors.push({1605 message: x.message,1606 componentStack: errorInfo.componentStack,1607 });1608 return 'Hash of (' + x.message + ')';1609 }1610 loggedErrors.length = 0;16111612 await act(() => {1613 const {pipe} = renderToPipeableStream(<App />, {1614 onError,1615 });1616 pipe(writable);1617 });16181619 expect(loggedErrors.length).toBe(1);1620 expect(loggedErrors[0].message).toBe('Bye');1621 expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(1622 componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),1623 );1624 });16251626 it('should asynchronously load a lazy element', async () => {1627 let resolveElement;1628 const lazyElement = React.lazy(() => {1629 return new Promise(r => {1630 resolveElement = r;1631 });1632 });16331634 await act(() => {1635 const {pipe} = renderToPipeableStream(1636 <div>1637 <Suspense fallback={<Text text="Loading..." />}>1638 {lazyElement}1639 </Suspense>1640 </div>,1641 );1642 pipe(writable);1643 });1644 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1645 // Because there is no content inside the Suspense boundary that could've1646 // been written, we expect to not see any additional partial data flushed1647 // yet.1648 expect(1649 stripExternalRuntimeInNodes(1650 container.childNodes,1651 renderOptions.unstable_externalRuntimeSrc,1652 ).length,1653 ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);1654 await act(() => {1655 resolveElement({default: <Text text="Hello" />});1656 });1657 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1658 });16591660 it('should client render a boundary if a lazy element rejects', async () => {1661 let rejectElement;1662 const element = <Text text="Hello" />;1663 const lazyElement = React.lazy(() => {1664 return new Promise((resolve, reject) => {1665 rejectElement = reject;1666 });1667 });16681669 const theError = new Error('Test');1670 const loggedErrors = [];1671 function onError(x, errorInfo) {1672 loggedErrors.push(x);1673 return 'hash of (' + x.message + ')';1674 }1675 const expectedDigest = onError(theError);1676 loggedErrors.length = 0;16771678 function App({isClient}) {1679 return (1680 <div>1681 <Suspense fallback={<Text text="Loading..." />}>1682 {isClient ? element : lazyElement}1683 </Suspense>1684 </div>1685 );1686 }16871688 await act(() => {1689 const {pipe} = renderToPipeableStream(<App isClient={false} />, {1690 onError,1691 });1692 pipe(writable);1693 });1694 expect(loggedErrors).toEqual([]);16951696 const errors = [];1697 // Attempt to hydrate the content.1698 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1699 onRecoverableError(error, errorInfo) {1700 errors.push({error, errorInfo});1701 },1702 });1703 await waitForAll([]);17041705 // We're still loading because we're waiting for the server to stream more content.1706 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17071708 expect(loggedErrors).toEqual([]);17091710 await act(() => {1711 rejectElement(theError);1712 });17131714 expect(loggedErrors).toEqual([theError]);17151716 // We haven't ran the client hydration yet.1717 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17181719 // Now we can client render it instead.1720 await waitForAll([]);17211722 expectErrors(1723 errors,1724 [1725 [1726 'Switched to client rendering because the server rendering errored:\n\n' +1727 theError.message,1728 expectedDigest,1729 componentStack(['Suspense', 'div', 'App']),1730 ],1731 ],1732 [1733 [1734 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1735 expectedDigest,1736 ],1737 ],1738 );17391740 // The client rendered HTML is now in place.1741 // expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);17421743 expect(loggedErrors).toEqual([theError]);1744 });17451746 it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {1747 function Indirection({level, children}) {1748 if (level > 0) {1749 return <Indirection level={level - 1}>{children}</Indirection>;1750 }1751 return children;1752 }17531754 const theError = new Error('uh oh');17551756 function Erroring({isClient}) {1757 if (isClient) {1758 return 'Hello World';1759 }1760 throw theError;1761 }17621763 function App({isClient}) {1764 return (1765 <div>1766 <Suspense fallback={<span>loading...</span>}>1767 <Indirection level={2}>1768 <Erroring isClient={isClient} />1769 </Indirection>1770 </Suspense>1771 </div>1772 );1773 }17741775 const loggedErrors = [];1776 function onError(x) {1777 loggedErrors.push(x);1778 return 'hash(' + x.message + ')';1779 }1780 const expectedDigest = onError(theError);1781 loggedErrors.length = 0;17821783 await act(() => {1784 const {pipe} = renderToPipeableStream(1785 <App />,17861787 {1788 onError,1789 },1790 );1791 pipe(writable);1792 });1793 expect(loggedErrors).toEqual([theError]);17941795 const errors = [];1796 // Attempt to hydrate the content.1797 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1798 onRecoverableError(error, errorInfo) {1799 errors.push({error, errorInfo});1800 },1801 });1802 await waitForAll([]);18031804 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);18051806 expectErrors(1807 errors,1808 [1809 [1810 'Switched to client rendering because the server rendering errored:\n\n' +1811 theError.message,1812 expectedDigest,1813 componentStack([1814 'Erroring',1815 'Indirection',1816 'Indirection',1817 'Indirection',1818 'Suspense',1819 'div',1820 'App',1821 ]),1822 ],1823 ],1824 [1825 [1826 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1827 expectedDigest,1828 ],1829 ],1830 );1831 });18321833 it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1834 let rejectComponent;1835 const LazyComponent = React.lazy(() => {1836 return new Promise((resolve, reject) => {1837 rejectComponent = reject;1838 });1839 });18401841 function App({isClient}) {1842 return (1843 <div>1844 <Suspense fallback={<Text text="Loading..." />}>1845 {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1846 </Suspense>1847 </div>1848 );1849 }18501851 const loggedErrors = [];1852 const theError = new Error('uh oh');1853 function onError(x) {1854 loggedErrors.push(x);1855 return 'hash(' + x.message + ')';1856 }1857 const expectedDigest = onError(theError);1858 loggedErrors.length = 0;18591860 await act(() => {1861 const {pipe} = renderToPipeableStream(1862 <App />,18631864 {1865 onError,1866 },1867 );1868 pipe(writable);1869 });1870 expect(loggedErrors).toEqual([]);18711872 const errors = [];1873 // Attempt to hydrate the content.1874 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1875 onRecoverableError(error, errorInfo) {1876 errors.push({error, errorInfo});1877 },1878 });1879 await waitForAll([]);18801881 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18821883 await act(() => {1884 rejectComponent(theError);1885 });18861887 expect(loggedErrors).toEqual([theError]);1888 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18891890 // Now we can client render it instead.1891 await waitForAll([]);18921893 expectErrors(1894 errors,1895 [1896 [1897 'Switched to client rendering because the server rendering errored:\n\n' +1898 theError.message,1899 expectedDigest,1900 componentStack(['Lazy', 'Suspense', 'div', 'App']),1901 ],1902 ],1903 [1904 [1905 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1906 expectedDigest,1907 ],1908 ],1909 );19101911 // The client rendered HTML is now in place.1912 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1913 expect(loggedErrors).toEqual([theError]);1914 });19151916 it('should asynchronously load the suspense boundary', async () => {1917 await act(() => {1918 const {pipe} = renderToPipeableStream(1919 <div>1920 <Suspense fallback={<Text text="Loading..." />}>1921 <AsyncText text="Hello World" />1922 </Suspense>1923 </div>,1924 );1925 pipe(writable);1926 });1927 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1928 await act(() => {1929 resolveText('Hello World');1930 });1931 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1932 });19331934 it('waits for pending content to come in from the server and then hydrates it', async () => {1935 const ref = React.createRef();19361937 function App() {1938 return (1939 <div>1940 <Suspense fallback="Loading...">1941 <h1 ref={ref}>1942 <AsyncText text="Hello" />1943 </h1>1944 </Suspense>1945 </div>1946 );1947 }19481949 let bootstrapped = false;1950 window.__INIT__ = function () {1951 bootstrapped = true;1952 // Attempt to hydrate the content.1953 ReactDOMClient.hydrateRoot(container, <App />);1954 };19551956 await act(() => {1957 const {pipe} = renderToPipeableStream(<App />, {1958 bootstrapScriptContent: '__INIT__();',1959 });1960 pipe(writable);1961 });19621963 // We're still showing a fallback.1964 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19651966 // We already bootstrapped.1967 expect(bootstrapped).toBe(true);19681969 // Attempt to hydrate the content.1970 await waitForAll([]);19711972 // We're still loading because we're waiting for the server to stream more content.1973 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19741975 // The server now updates the content in place in the fallback.1976 await act(() => {1977 resolveText('Hello');1978 });19791980 // The final HTML is now in place.1981 expect(getVisibleChildren(container)).toEqual(1982 <div>1983 <h1>Hello</h1>1984 </div>,1985 );1986 const h1 = container.getElementsByTagName('h1')[0];19871988 // But it is not yet hydrated.1989 expect(ref.current).toBe(null);19901991 await waitForAll([]);19921993 // Now it's hydrated.1994 expect(ref.current).toBe(h1);1995 });19961997 it('handles an error on the client if the server ends up erroring', async () => {1998 const ref = React.createRef();19992000 class ErrorBoundary extends React.Component {
Findings
✓ No findings reported for this file.