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 it('can opt a component into browser-only rendering', async () => {410 let resolveBrowserText;411 const browserText = new Promise(resolve => {412 resolveBrowserText = resolve;413 });414 let browserReason;415 const initializeReason = jest.fn(() => {416 browserReason = Object.freeze(417 new Error('Only render this content in a browser'),418 );419 return browserReason;420 });421 const browserOnly = ReactDOM.browser(initializeReason);422423 function BrowserOnly() {424 use(browserOnly);425 const text = use(browserText);426 Scheduler.log(text);427 return <span>{text}</span>;428 }429430 function App() {431 return (432 <div>433 <Suspense fallback={<span>Fallback</span>}>434 <BrowserOnly />435 </Suspense>436 </div>437 );438 }439440 const serverErrors = [];441 const browserBailouts = [];442 await act(() => {443 const {pipe} = renderToPipeableStream(<App />, {444 onError(error) {445 serverErrors.push(error);446 },447 onBrowserBailout(error, errorInfo) {448 browserBailouts.push({error, errorInfo});449 },450 });451 pipe(writable);452 });453454 expect(serverErrors).toEqual([]);455 expect(initializeReason).toHaveBeenCalledTimes(1);456 expect(browserBailouts).toHaveLength(1);457 expect(browserBailouts[0].error).toBeInstanceOf(Error);458 expect(browserBailouts[0].error.message).toBe(459 'Browser-only rendering was requested by `browser()`.',460 );461 expect(browserBailouts[0].error.stack).toContain('BrowserOnly');462 expect(browserBailouts[0].error.cause).toBe(browserReason);463 expect(464 normalizeCodeLocInfo(browserBailouts[0].errorInfo.componentStack),465 ).toBe(componentStack(['BrowserOnly', 'Suspense', 'div', 'App']));466 expect(getVisibleChildren(container)).toEqual(467 <div>468 <span>Fallback</span>469 </div>,470 );471 const recoverableErrors = [];472 ReactDOMClient.hydrateRoot(container, <App />, {473 onRecoverableError(error) {474 recoverableErrors.push(error);475 },476 });477 await waitForAll([]);478479 expect(getVisibleChildren(container)).toEqual(480 <div>481 <span>Fallback</span>482 </div>,483 );484485 await clientAct(() => {486 resolveBrowserText('Browser');487 });488 assertLog(['Browser']);489490 expect(recoverableErrors).toEqual([]);491 expect(initializeReason).toHaveBeenCalledTimes(1);492 expect(getVisibleChildren(container)).toEqual(493 <div>494 <span>Browser</span>495 </div>,496 );497 });498499 it('can opt a component into browser-only rendering after streaming the fallback', async () => {500 let resolveServerReady;501 const serverReady = new Promise(resolve => {502 resolveServerReady = resolve;503 });504 const initializeReason = jest.fn(505 () => 'Only render this content in a browser',506 );507508 function BrowserOnly() {509 use(serverReady);510 use(ReactDOM.browser(initializeReason));511 return <span>Browser</span>;512 }513514 function App() {515 return (516 <div>517 <Suspense fallback={<span>Fallback</span>}>518 <BrowserOnly />519 </Suspense>520 </div>521 );522 }523524 const serverErrors = [];525 const browserBailouts = [];526 await act(() => {527 const {pipe} = renderToPipeableStream(<App />, {528 onError(error) {529 serverErrors.push(error);530 },531 onBrowserBailout(error) {532 browserBailouts.push(error);533 },534 });535 pipe(writable);536 });537538 expect(getVisibleChildren(container)).toEqual(539 <div>540 <span>Fallback</span>541 </div>,542 );543544 await act(() => {545 resolveServerReady();546 });547548 expect(serverErrors).toEqual([]);549 expect(initializeReason).toHaveBeenCalledTimes(1);550 expect(browserBailouts).toHaveLength(1);551 expect(browserBailouts[0].message).toBe(552 'Browser-only rendering was requested by `browser()`.',553 );554 expect(browserBailouts[0].stack).toContain('BrowserOnly');555 expect(browserBailouts[0].cause).toBe(556 'Only render this content in a browser',557 );558559 const recoverableErrors = [];560 ReactDOMClient.hydrateRoot(container, <App />, {561 onRecoverableError(error) {562 recoverableErrors.push(error);563 },564 });565 await waitForAll([]);566567 expect(recoverableErrors).toEqual([]);568 expect(initializeReason).toHaveBeenCalledTimes(1);569 expect(getVisibleChildren(container)).toEqual(570 <div>571 <span>Browser</span>572 </div>,573 );574 });575576 it('supports omitted and direct string browser reasons', async () => {577 const directReason = 'Only render this content in a browser';578 const withoutReason = ReactDOM.browser();579 const withDirectReason = ReactDOM.browser(directReason);580581 function WithoutReason() {582 use(withoutReason);583 return <span>Browser</span>;584 }585586 function WithDirectReason() {587 use(withDirectReason);588 return <span>Browser</span>;589 }590591 const serverErrors = [];592 const browserBailouts = [];593 await act(() => {594 const {pipe} = renderToPipeableStream(595 <>596 <Suspense fallback={<span>Fallback A</span>}>597 <WithoutReason />598 </Suspense>599 <Suspense fallback={<span>Fallback B</span>}>600 <WithDirectReason />601 </Suspense>602 </>,603 {604 onError(error) {605 serverErrors.push(error);606 },607 onBrowserBailout(error) {608 browserBailouts.push(error);609 },610 },611 );612 pipe(writable);613 });614615 expect(serverErrors).toEqual([]);616 expect(browserBailouts).toHaveLength(2);617 expect(browserBailouts[0].message).toBe(618 'Browser-only rendering was requested by `browser()`.',619 );620 expect(browserBailouts[0].stack).toContain('WithoutReason');621 expect(622 Object.prototype.hasOwnProperty.call(browserBailouts[0], 'cause'),623 ).toBe(false);624 expect(browserBailouts[1].message).toBe(625 'Browser-only rendering was requested by `browser()`.',626 );627 expect(browserBailouts[1].stack).toContain('WithDirectReason');628 expect(browserBailouts[1].cause).toBe(directReason);629 });630631 it('supports any value returned by a browser reason initializer', async () => {632 const reasonValues = [undefined, null, 42, Symbol('browser reason')];633 const initializeReasons = reasonValues.map(reason => jest.fn(() => reason));634 const browserValues = initializeReasons.map(initializeReason =>635 ReactDOM.browser(initializeReason),636 );637638 function BrowserOnly({browserValue}) {639 use(browserValue);640 return <span>Browser</span>;641 }642643 const serverErrors = [];644 const browserBailouts = [];645 await act(() => {646 const {pipe} = renderToPipeableStream(647 <>648 {browserValues.map((browserValue, index) => (649 <Suspense key={index} fallback={<span>Fallback</span>}>650 <BrowserOnly browserValue={browserValue} />651 </Suspense>652 ))}653 </>,654 {655 onError(error) {656 serverErrors.push(error);657 },658 onBrowserBailout(error) {659 browserBailouts.push(error);660 },661 },662 );663 pipe(writable);664 });665666 expect(serverErrors).toEqual([]);667 expect(browserBailouts).toHaveLength(reasonValues.length);668 initializeReasons.forEach(initializeReason => {669 expect(initializeReason).toHaveBeenCalledTimes(1);670 });671 browserBailouts.forEach((error, index) => {672 expect(error).toBeInstanceOf(Error);673 expect(error.message).toBe(674 'Browser-only rendering was requested by `browser()`.',675 );676 expect(Object.prototype.hasOwnProperty.call(error, 'cause')).toBe(true);677 expect(error.cause).toBe(reasonValues[index]);678 });679 });680681 it('initializes a shared browser reason at each use site', async () => {682 const browserReasons = [];683 const initializeReason = jest.fn(() => {684 const browserReason = {index: browserReasons.length};685 browserReasons.push(browserReason);686 return browserReason;687 });688 const browserValue = ReactDOM.browser(initializeReason);689690 function BrowserOnlyA() {691 use(browserValue);692 return <span>Browser A</span>;693 }694695 function BrowserOnlyB() {696 use(browserValue);697 return <span>Browser B</span>;698 }699700 const browserBailouts = [];701 await act(() => {702 const {pipe} = renderToPipeableStream(703 <>704 <Suspense fallback={<span>Fallback A</span>}>705 <BrowserOnlyA />706 </Suspense>707 <Suspense fallback={<span>Fallback B</span>}>708 <BrowserOnlyB />709 </Suspense>710 </>,711 {712 onBrowserBailout(error) {713 browserBailouts.push(error);714 },715 },716 );717 pipe(writable);718 });719720 expect(initializeReason).toHaveBeenCalledTimes(2);721 expect(browserBailouts).toHaveLength(2);722 expect(browserBailouts[0]).not.toBe(browserBailouts[1]);723 expect(browserBailouts[0].cause).toBe(browserReasons[0]);724 expect(browserBailouts[0].stack).toContain('BrowserOnlyA');725 expect(browserBailouts[1].cause).toBe(browserReasons[1]);726 expect(browserBailouts[1].stack).toContain('BrowserOnlyB');727 });728729 it('uses a fallback if a browser reason initializer throws', async () => {730 const reasonError = new Error('Failed to initialize browser reason');731 const initializeReason = jest.fn(() => {732 throw reasonError;733 });734 const browserValue = ReactDOM.browser(initializeReason);735736 function BrowserOnly() {737 use(browserValue);738 return <span>Browser</span>;739 }740741 const serverErrors = [];742 const browserBailouts = [];743 await act(() => {744 const {pipe} = renderToPipeableStream(745 <Suspense fallback={<span>Fallback</span>}>746 <BrowserOnly />747 </Suspense>,748 {749 onError(error) {750 serverErrors.push(error);751 },752 onBrowserBailout(error) {753 browserBailouts.push(error);754 },755 },756 );757 pipe(writable);758 });759760 expect(initializeReason).toHaveBeenCalledTimes(1);761 expect(serverErrors).toEqual([]);762 expect(browserBailouts).toHaveLength(1);763 expect(browserBailouts[0].cause).toBe(764 'The reason for browser-only rendering could not be determined because ' +765 'its initializer threw.',766 );767 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);768 });769770 it('errors if browser-only content is rendered outside Suspense', async () => {771 const browserReason = 'Only render this content in a browser';772 const browserValue = ReactDOM.browser(browserReason);773774 function BrowserOnly() {775 use(browserValue);776 return <span>Browser</span>;777 }778779 const reportedErrors = [];780 const browserBailouts = [];781 let shellReady = false;782 let shellError;783 await act(() => {784 renderToPipeableStream(<BrowserOnly />, {785 onError(error) {786 reportedErrors.push(error);787 },788 onBrowserBailout(error) {789 browserBailouts.push(error);790 },791 onShellReady() {792 shellReady = true;793 },794 onShellError(error) {795 shellError = error;796 },797 });798 });799800 expect(shellError).toBeInstanceOf(Error);801 expect(shellError.message).toBe(802 'The server render could not complete because client rendering was ' +803 "requested outside a Suspense boundary. See this error's cause for " +804 'additional details.',805 );806 expect(shellError.cause).toBe(browserReason);807 expect(shellError.stack).toContain('BrowserOnly');808 expect(shellError.stack.split('\n')[0]).toBe(809 'Error: ' + shellError.message,810 );811 expect(shellReady).toBe(false);812 expect(reportedErrors).toEqual([shellError]);813 expect(browserBailouts).toEqual([]);814 });815816 it('can abort all pending boundaries into browser-only rendering', async () => {817 const never = new Promise(() => {});818 let isClient = false;819820 function Pending({children}) {821 if (!isClient) {822 use(never);823 }824 return <span>{children}</span>;825 }826827 function App() {828 return (829 <div>830 <span>Shell</span>831 <Suspense fallback={<span>Loading A</span>}>832 <Pending>A</Pending>833 </Suspense>834 <Suspense fallback={<span>Loading B</span>}>835 <Pending>B</Pending>836 </Suspense>837 </div>838 );839 }840841 const serverErrors = [];842 const browserBailouts = [];843 const browserReason = {code: 'render-pending-content-in-browser'};844 const initializeReason = jest.fn(() => browserReason);845 const browserValue = ReactDOM.browser(initializeReason);846 let abort;847 await act(() => {848 const controls = renderToPipeableStream(<App />, {849 onError(error) {850 serverErrors.push(error);851 },852 onBrowserBailout(error) {853 browserBailouts.push(error);854 },855 });856 abort = controls.abort;857 controls.pipe(writable);858 });859860 expect(getVisibleChildren(container)).toEqual(861 <div>862 <span>Shell</span>863 <span>Loading A</span>864 <span>Loading B</span>865 </div>,866 );867868 await act(() => {869 function abortToBrowser() {870 abort(browserValue);871 }872 abortToBrowser();873 });874875 expect(serverErrors).toEqual([]);876 expect(initializeReason).toHaveBeenCalledTimes(1);877 expect(browserBailouts).toHaveLength(2);878 expect(browserBailouts[0]).toBeInstanceOf(Error);879 expect(browserBailouts[0].message).toBe(880 'Browser-only rendering was requested by `browser()`.',881 );882 expect(browserBailouts[0].stack).toContain('abortToBrowser');883 expect(browserBailouts[0].cause).toBe(browserReason);884 expect(browserBailouts[1]).toBe(browserBailouts[0]);885886 isClient = true;887 const recoverableErrors = [];888 ReactDOMClient.hydrateRoot(container, <App />, {889 onRecoverableError(error) {890 recoverableErrors.push(error);891 },892 });893 await waitForAll([]);894895 expect(recoverableErrors).toEqual([]);896 expect(getVisibleChildren(container)).toEqual(897 <div>898 <span>Shell</span>899 <span>A</span>900 <span>B</span>901 </div>,902 );903 });904905 it('errors if aborted with browser() before the shell completes', async () => {906 const never = new Promise(() => {});907 let browserReason;908 const initializeReason = jest.fn(() => {909 browserReason = new Error('Only abort this render on the server');910 return browserReason;911 });912 const browserValue = ReactDOM.browser(initializeReason);913914 function PendingRoot() {915 use(never);916 return <span>Root</span>;917 }918919 const reportedErrors = [];920 const browserBailouts = [];921 let shellReady = false;922 let shellError;923 let abort;924 await act(() => {925 const controls = renderToPipeableStream(<PendingRoot />, {926 onError(error) {927 reportedErrors.push(error);928 },929 onBrowserBailout(error) {930 browserBailouts.push(error);931 },932 onShellReady() {933 shellReady = true;934 },935 onShellError(error) {936 shellError = error;937 },938 });939 abort = controls.abort;940 });941942 await act(() => {943 function abortToBrowser() {944 abort(browserValue);945 }946 abortToBrowser();947 });948949 expect(shellError).toBeInstanceOf(Error);950 expect(initializeReason).toHaveBeenCalledTimes(1);951 expect(shellError.message).toBe(952 'The server render could not complete because client rendering was ' +953 "requested outside a Suspense boundary. See this error's cause for " +954 'additional details.',955 );956 expect(shellError.cause).toBe(browserReason);957 expect(shellError.stack).toContain('abortToBrowser');958 expect(shellReady).toBe(false);959 expect(reportedErrors).toEqual([shellError]);960 expect(browserBailouts).toEqual([]);961 });962963 it('reports nested browser bailouts if aborting fatals the shell', async () => {964 const never = new Promise(() => {});965 const browserReason = 'Abort pending work into browser rendering';966 const browserValue = ReactDOM.browser(browserReason);967968 function Pending() {969 use(never);970 return <span>Pending</span>;971 }972973 const reportedErrors = [];974 const browserBailouts = [];975 let shellError;976 let abort;977 await act(() => {978 const controls = renderToPipeableStream(979 <>980 <Suspense fallback={<span>Fallback</span>}>981 <Pending />982 </Suspense>983 <Pending />984 <Suspense fallback={<span>Fallback</span>}>985 <Pending />986 </Suspense>987 <Pending />988 </>,989 {990 onError(error) {991 reportedErrors.push(error);992 },993 onBrowserBailout(error) {994 browserBailouts.push(error);995 },996 onShellError(error) {997 shellError = error;998 },999 },1000 );1001 abort = controls.abort;1002 });10031004 await act(() => {1005 abort(browserValue);1006 });10071008 expect(shellError).toBeInstanceOf(Error);1009 expect(shellError.message).toBe(1010 'The server render could not complete because client rendering was ' +1011 "requested outside a Suspense boundary. See this error's cause for " +1012 'additional details.',1013 );1014 expect(shellError.cause).toBe(browserReason);1015 expect(reportedErrors).toHaveLength(2);1016 expect(reportedErrors[0]).toBe(shellError);1017 expect(reportedErrors[1].message).toBe(shellError.message);1018 expect(reportedErrors[1].cause).toBe(browserReason);1019 expect(browserBailouts).toHaveLength(2);1020 expect(browserBailouts[0]).toBe(browserBailouts[1]);1021 expect(browserBailouts[0]).not.toBe(shellError);1022 expect(browserBailouts[0].message).toBe(1023 'Browser-only rendering was requested by `browser()`.',1024 );1025 expect(browserBailouts[0].cause).toBe(browserReason);1026 });10271028 it('uses a fallback if a browser reason initializer throws during abort', async () => {1029 const never = new Promise(() => {});1030 const reasonError = new Error('Failed to initialize browser reason');1031 const initializeReason = jest.fn(() => {1032 throw reasonError;1033 });1034 const browserValue = ReactDOM.browser(initializeReason);10351036 function PendingRoot() {1037 use(never);1038 return <span>Root</span>;1039 }10401041 const reportedErrors = [];1042 const browserBailouts = [];1043 let shellError;1044 let abort;1045 await act(() => {1046 const controls = renderToPipeableStream(<PendingRoot />, {1047 onError(error) {1048 reportedErrors.push(error);1049 },1050 onBrowserBailout(error) {1051 browserBailouts.push(error);1052 },1053 onShellError(error) {1054 shellError = error;1055 },1056 });1057 abort = controls.abort;1058 });10591060 await act(() => {1061 abort(browserValue);1062 });10631064 expect(initializeReason).toHaveBeenCalledTimes(1);1065 expect(shellError).toBeInstanceOf(Error);1066 expect(shellError.cause).toBe(1067 'The reason for browser-only rendering could not be determined because ' +1068 'its initializer threw.',1069 );1070 expect(reportedErrors).toEqual([shellError]);1071 expect(browserBailouts).toEqual([]);1072 });10731074 it('reports the browser value if it is thrown instead of passed to use', async () => {1075 const initializeReason = jest.fn(1076 () => new Error('Only render this content in a browser'),1077 );1078 const browserValue = ReactDOM.browser(initializeReason);10791080 function BrowserOnly() {1081 throw browserValue;1082 }10831084 const reportedErrors = [];1085 const browserBailouts = [];1086 await act(() => {1087 const {pipe} = renderToPipeableStream(1088 <Suspense fallback={<span>Fallback</span>}>1089 <BrowserOnly />1090 </Suspense>,1091 {1092 onError(error) {1093 reportedErrors.push(error);1094 },1095 onBrowserBailout(error) {1096 browserBailouts.push(error);1097 },1098 },1099 );1100 pipe(writable);1101 });11021103 expect(reportedErrors).toEqual([browserValue]);1104 expect(browserBailouts).toEqual([]);1105 expect(initializeReason).not.toHaveBeenCalled();1106 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);1107 });11081109 ['', 'BROWSER'].forEach(userDigest => {1110 it(`does not reserve the ${JSON.stringify(1111 userDigest,1112 )} user error digest for browser rendering`, async () => {1113 let isClient = false;1114 const serverError = new Error('Server error');11151116 function ServerError() {1117 if (!isClient) {1118 throw serverError;1119 }1120 return <span>Client</span>;1121 }11221123 function App() {1124 return (1125 <Suspense fallback={<span>Fallback</span>}>1126 <ServerError />1127 </Suspense>1128 );1129 }11301131 const serverErrors = [];1132 await act(() => {1133 const {pipe} = renderToPipeableStream(<App />, {1134 onError(error) {1135 serverErrors.push(error);1136 return userDigest;1137 },1138 });1139 pipe(writable);1140 });11411142 expect(serverErrors).toEqual([serverError]);1143 expect(getVisibleChildren(container)).toEqual(<span>Fallback</span>);11441145 isClient = true;1146 const recoverableErrors = [];1147 ReactDOMClient.hydrateRoot(container, <App />, {1148 onRecoverableError(error) {1149 recoverableErrors.push(error);1150 },1151 });1152 await waitForAll([]);11531154 expect(recoverableErrors).toHaveLength(1);1155 expect(recoverableErrors[0].digest).toBe(userDigest || undefined);1156 expect(getVisibleChildren(container)).toEqual(<span>Client</span>);1157 });1158 });11591160 it('should asynchronously load a lazy component', async () => {1161 let resolveA;1162 const LazyA = React.lazy(() => {1163 return new Promise(r => {1164 resolveA = r;1165 });1166 });11671168 let resolveB;1169 const LazyB = React.lazy(() => {1170 return new Promise(r => {1171 resolveB = r;1172 });1173 });11741175 class TextWithPunctuation extends React.Component {1176 render() {1177 return <Text text={this.props.text + this.props.punctuation} />;1178 }1179 }11801181 // This tests that default props of the inner element is resolved.1182 TextWithPunctuation.defaultProps = {1183 punctuation: '!',1184 };11851186 await act(() => {1187 const {pipe} = renderToPipeableStream(1188 <div>1189 <div>1190 <Suspense fallback={<Text text="Loading..." />}>1191 <LazyA text="Hello" />1192 </Suspense>1193 </div>1194 <div>1195 <Suspense fallback={<Text text="Loading..." />}>1196 <LazyB text="world" />1197 </Suspense>1198 </div>1199 </div>,1200 );1201 pipe(writable);1202 });12031204 expect(getVisibleChildren(container)).toEqual(1205 <div>1206 <div>Loading...</div>1207 <div>Loading...</div>1208 </div>,1209 );1210 await act(() => {1211 resolveA({default: Text});1212 });1213 expect(getVisibleChildren(container)).toEqual(1214 <div>1215 <div>Hello</div>1216 <div>Loading...</div>1217 </div>,1218 );1219 await act(() => {1220 resolveB({default: TextWithPunctuation});1221 });1222 expect(getVisibleChildren(container)).toEqual(1223 <div>1224 <div>Hello</div>1225 <div>world!</div>1226 </div>,1227 );1228 });12291230 it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {1231 const makeApp = () => {1232 let resolve;1233 const imports = new Promise(r => {1234 resolve = () => r({default: () => <span id="async">async</span>});1235 });1236 const Lazy = React.lazy(() => imports);12371238 const App = () => (1239 <div>1240 <Suspense fallback={<span>Loading...</span>}>1241 <Lazy />1242 <span id="after">after</span>1243 </Suspense>1244 </div>1245 );12461247 return [App, resolve];1248 };12491250 // Server-side1251 const [App, resolve] = makeApp();1252 await act(() => {1253 const {pipe} = renderToPipeableStream(<App />);1254 pipe(writable);1255 });1256 expect(getVisibleChildren(container)).toEqual(1257 <div>1258 <span>Loading...</span>1259 </div>,1260 );1261 await act(() => {1262 resolve();1263 });1264 expect(getVisibleChildren(container)).toEqual(1265 <div>1266 <span id="async">async</span>1267 <span id="after">after</span>1268 </div>,1269 );12701271 // Client-side1272 const [HydrateApp, hydrateResolve] = makeApp();1273 await act(() => {1274 ReactDOMClient.hydrateRoot(container, <HydrateApp />);1275 });12761277 expect(getVisibleChildren(container)).toEqual(1278 <div>1279 <span id="async">async</span>1280 <span id="after">after</span>1281 </div>,1282 );12831284 await act(() => {1285 hydrateResolve();1286 });1287 expect(getVisibleChildren(container)).toEqual(1288 <div>1289 <span id="async">async</span>1290 <span id="after">after</span>1291 </div>,1292 );1293 });12941295 it('should support nonce for bootstrap and runtime scripts', async () => {1296 CSPnonce = 'R4nd0m';1297 try {1298 let resolve;1299 const Lazy = React.lazy(() => {1300 return new Promise(r => {1301 resolve = r;1302 });1303 });13041305 await act(() => {1306 const {pipe} = renderToPipeableStream(1307 <div>1308 <Suspense fallback={<Text text="Loading..." />}>1309 <Lazy text="Hello" />1310 </Suspense>1311 </div>,1312 {1313 nonce: 'R4nd0m',1314 bootstrapScriptContent: 'function noop(){}',1315 bootstrapScripts: [1316 'init.js',1317 {src: 'init2.js', integrity: 'init2hash'},1318 ],1319 bootstrapModules: [1320 'init.mjs',1321 {src: 'init2.mjs', integrity: 'init2hash'},1322 ],1323 },1324 );1325 pipe(writable);1326 });13271328 expect(getVisibleChildren(container)).toEqual([1329 <link1330 rel="preload"1331 fetchpriority="low"1332 href="init.js"1333 as="script"1334 nonce={CSPnonce}1335 />,1336 <link1337 rel="preload"1338 fetchpriority="low"1339 href="init2.js"1340 as="script"1341 nonce={CSPnonce}1342 integrity="init2hash"1343 />,1344 <link1345 rel="modulepreload"1346 fetchpriority="low"1347 href="init.mjs"1348 nonce={CSPnonce}1349 />,1350 <link1351 rel="modulepreload"1352 fetchpriority="low"1353 href="init2.mjs"1354 nonce={CSPnonce}1355 integrity="init2hash"1356 />,1357 <div>Loading...</div>,1358 ]);13591360 // check that there are 6 scripts with a matching nonce:1361 // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules1362 expect(1363 Array.from(container.getElementsByTagName('script')).filter(1364 node => node.getAttribute('nonce') === CSPnonce,1365 ).length,1366 ).toEqual(6);13671368 await act(() => {1369 resolve({default: Text});1370 });1371 expect(getVisibleChildren(container)).toEqual([1372 <link1373 rel="preload"1374 fetchpriority="low"1375 href="init.js"1376 as="script"1377 nonce={CSPnonce}1378 />,1379 <link1380 rel="preload"1381 fetchpriority="low"1382 href="init2.js"1383 as="script"1384 nonce={CSPnonce}1385 integrity="init2hash"1386 />,1387 <link1388 rel="modulepreload"1389 fetchpriority="low"1390 href="init.mjs"1391 nonce={CSPnonce}1392 />,1393 <link1394 rel="modulepreload"1395 fetchpriority="low"1396 href="init2.mjs"1397 nonce={CSPnonce}1398 integrity="init2hash"1399 />,1400 <div>Hello</div>,1401 ]);1402 } finally {1403 CSPnonce = null;1404 }1405 });14061407 it('should not automatically add nonce to rendered scripts', async () => {1408 CSPnonce = 'R4nd0m';1409 try {1410 await act(async () => {1411 const {pipe} = renderToPipeableStream(1412 <html>1413 <body>1414 <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>1415 <script nonce={CSPnonce} src="foo" async={true} />1416 <script src="bar" />1417 <script src="baz" integrity="qux" async={true} />1418 <script type="module" src="quux" async={true} />1419 <script type="module" src="corge" async={true} />1420 <script1421 type="module"1422 src="grault"1423 integrity="garply"1424 async={true}1425 />1426 </body>1427 </html>,1428 {1429 nonce: CSPnonce,1430 },1431 );1432 pipe(writable);1433 });14341435 expect(1436 stripExternalRuntimeInNodes(1437 document.getElementsByTagName('script'),1438 renderOptions.unstable_externalRuntimeSrc,1439 ).map(n => n.outerHTML),1440 ).toEqual([1441 `<script nonce="${CSPnonce}" src="foo" async=""></script>`,1442 `<script src="baz" integrity="qux" async=""></script>`,1443 `<script type="module" src="quux" async=""></script>`,1444 `<script type="module" src="corge" async=""></script>`,1445 `<script type="module" src="grault" integrity="garply" async=""></script>`,1446 `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,1447 `<script src="bar"></script>`,1448 ]);1449 } finally {1450 CSPnonce = null;1451 }1452 });14531454 it('should client render a boundary if a lazy component rejects', async () => {1455 let rejectComponent;1456 const promise = new Promise((resolve, reject) => {1457 rejectComponent = reject;1458 });1459 const LazyComponent = React.lazy(() => {1460 return promise;1461 });14621463 const LazyLazy = React.lazy(async () => {1464 return {1465 default: LazyComponent,1466 };1467 });14681469 function Wrapper({children}) {1470 return children;1471 }1472 const LazyWrapper = React.lazy(() => {1473 return {1474 then(callback) {1475 callback({1476 default: Wrapper,1477 });1478 },1479 };1480 });14811482 function App({isClient}) {1483 return (1484 <div>1485 <Suspense fallback={<Text text="Loading..." />}>1486 <LazyWrapper>1487 {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}1488 </LazyWrapper>1489 </Suspense>1490 </div>1491 );1492 }14931494 let bootstrapped = false;1495 const errors = [];1496 window.__INIT__ = function () {1497 bootstrapped = true;1498 // Attempt to hydrate the content.1499 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1500 onRecoverableError(error, errorInfo) {1501 errors.push({error, errorInfo});1502 },1503 });1504 };15051506 const theError = new Error('Test');1507 const loggedErrors = [];1508 function onError(x, errorInfo) {1509 loggedErrors.push(x);1510 return 'Hash of (' + x.message + ')';1511 }1512 const expectedDigest = onError(theError);1513 loggedErrors.length = 0;15141515 await act(() => {1516 const {pipe} = renderToPipeableStream(<App isClient={false} />, {1517 bootstrapScriptContent: '__INIT__();',1518 onError,1519 });1520 pipe(writable);1521 });15221523 expect(loggedErrors).toEqual([]);1524 expect(bootstrapped).toBe(true);15251526 await waitForAll([]);15271528 // We're still loading because we're waiting for the server to stream more content.1529 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15301531 expect(loggedErrors).toEqual([]);15321533 await act(() => {1534 rejectComponent(theError);1535 });15361537 expect(loggedErrors).toEqual([theError]);15381539 // We haven't ran the client hydration yet.1540 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);15411542 // Now we can client render it instead.1543 await waitForAll([]);1544 expectErrors(1545 errors,1546 [1547 [1548 'Switched to client rendering because the server rendering errored:\n\n' +1549 theError.message,1550 expectedDigest,1551 componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),1552 ],1553 ],1554 [1555 [1556 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1557 expectedDigest,1558 ],1559 ],1560 );15611562 // The client rendered HTML is now in place.1563 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);15641565 expect(loggedErrors).toEqual([theError]);1566 });15671568 it('should have special stacks if Suspense fallback', async () => {1569 const infinitePromise = new Promise(() => {});1570 const InfiniteComponent = React.lazy(() => {1571 return infinitePromise;1572 });15731574 function Throw({text}) {1575 throw new Error(text);1576 }15771578 function App() {1579 return (1580 <Suspense fallback="Loading">1581 <div>1582 <Suspense fallback={<Throw text="Bye" />}>1583 <InfiniteComponent text="Hi" />1584 </Suspense>1585 </div>1586 </Suspense>1587 );1588 }15891590 const loggedErrors = [];1591 function onError(x, errorInfo) {1592 loggedErrors.push({1593 message: x.message,1594 componentStack: errorInfo.componentStack,1595 });1596 return 'Hash of (' + x.message + ')';1597 }1598 loggedErrors.length = 0;15991600 await act(() => {1601 const {pipe} = renderToPipeableStream(<App />, {1602 onError,1603 });1604 pipe(writable);1605 });16061607 expect(loggedErrors.length).toBe(1);1608 expect(loggedErrors[0].message).toBe('Bye');1609 expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(1610 componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),1611 );1612 });16131614 it('should asynchronously load a lazy element', async () => {1615 let resolveElement;1616 const lazyElement = React.lazy(() => {1617 return new Promise(r => {1618 resolveElement = r;1619 });1620 });16211622 await act(() => {1623 const {pipe} = renderToPipeableStream(1624 <div>1625 <Suspense fallback={<Text text="Loading..." />}>1626 {lazyElement}1627 </Suspense>1628 </div>,1629 );1630 pipe(writable);1631 });1632 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1633 // Because there is no content inside the Suspense boundary that could've1634 // been written, we expect to not see any additional partial data flushed1635 // yet.1636 expect(1637 stripExternalRuntimeInNodes(1638 container.childNodes,1639 renderOptions.unstable_externalRuntimeSrc,1640 ).length,1641 ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);1642 await act(() => {1643 resolveElement({default: <Text text="Hello" />});1644 });1645 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1646 });16471648 it('should client render a boundary if a lazy element rejects', async () => {1649 let rejectElement;1650 const element = <Text text="Hello" />;1651 const lazyElement = React.lazy(() => {1652 return new Promise((resolve, reject) => {1653 rejectElement = reject;1654 });1655 });16561657 const theError = new Error('Test');1658 const loggedErrors = [];1659 function onError(x, errorInfo) {1660 loggedErrors.push(x);1661 return 'hash of (' + x.message + ')';1662 }1663 const expectedDigest = onError(theError);1664 loggedErrors.length = 0;16651666 function App({isClient}) {1667 return (1668 <div>1669 <Suspense fallback={<Text text="Loading..." />}>1670 {isClient ? element : lazyElement}1671 </Suspense>1672 </div>1673 );1674 }16751676 await act(() => {1677 const {pipe} = renderToPipeableStream(<App isClient={false} />, {1678 onError,1679 });1680 pipe(writable);1681 });1682 expect(loggedErrors).toEqual([]);16831684 const errors = [];1685 // Attempt to hydrate the content.1686 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1687 onRecoverableError(error, errorInfo) {1688 errors.push({error, errorInfo});1689 },1690 });1691 await waitForAll([]);16921693 // We're still loading because we're waiting for the server to stream more content.1694 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);16951696 expect(loggedErrors).toEqual([]);16971698 await act(() => {1699 rejectElement(theError);1700 });17011702 expect(loggedErrors).toEqual([theError]);17031704 // We haven't ran the client hydration yet.1705 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);17061707 // Now we can client render it instead.1708 await waitForAll([]);17091710 expectErrors(1711 errors,1712 [1713 [1714 'Switched to client rendering because the server rendering errored:\n\n' +1715 theError.message,1716 expectedDigest,1717 componentStack(['Suspense', 'div', 'App']),1718 ],1719 ],1720 [1721 [1722 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1723 expectedDigest,1724 ],1725 ],1726 );17271728 // The client rendered HTML is now in place.1729 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);17301731 expect(loggedErrors).toEqual([theError]);1732 });17331734 it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {1735 function Indirection({level, children}) {1736 if (level > 0) {1737 return <Indirection level={level - 1}>{children}</Indirection>;1738 }1739 return children;1740 }17411742 const theError = new Error('uh oh');17431744 function Erroring({isClient}) {1745 if (isClient) {1746 return 'Hello World';1747 }1748 throw theError;1749 }17501751 function App({isClient}) {1752 return (1753 <div>1754 <Suspense fallback={<span>loading...</span>}>1755 <Indirection level={2}>1756 <Erroring isClient={isClient} />1757 </Indirection>1758 </Suspense>1759 </div>1760 );1761 }17621763 const loggedErrors = [];1764 function onError(x) {1765 loggedErrors.push(x);1766 return 'hash(' + x.message + ')';1767 }1768 const expectedDigest = onError(theError);1769 loggedErrors.length = 0;17701771 await act(() => {1772 const {pipe} = renderToPipeableStream(1773 <App />,17741775 {1776 onError,1777 },1778 );1779 pipe(writable);1780 });1781 expect(loggedErrors).toEqual([theError]);17821783 const errors = [];1784 // Attempt to hydrate the content.1785 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1786 onRecoverableError(error, errorInfo) {1787 errors.push({error, errorInfo});1788 },1789 });1790 await waitForAll([]);17911792 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);17931794 expectErrors(1795 errors,1796 [1797 [1798 'Switched to client rendering because the server rendering errored:\n\n' +1799 theError.message,1800 expectedDigest,1801 componentStack([1802 'Erroring',1803 'Indirection',1804 'Indirection',1805 'Indirection',1806 'Suspense',1807 'div',1808 'App',1809 ]),1810 ],1811 ],1812 [1813 [1814 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1815 expectedDigest,1816 ],1817 ],1818 );1819 });18201821 it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1822 let rejectComponent;1823 const LazyComponent = React.lazy(() => {1824 return new Promise((resolve, reject) => {1825 rejectComponent = reject;1826 });1827 });18281829 function App({isClient}) {1830 return (1831 <div>1832 <Suspense fallback={<Text text="Loading..." />}>1833 {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1834 </Suspense>1835 </div>1836 );1837 }18381839 const loggedErrors = [];1840 const theError = new Error('uh oh');1841 function onError(x) {1842 loggedErrors.push(x);1843 return 'hash(' + x.message + ')';1844 }1845 const expectedDigest = onError(theError);1846 loggedErrors.length = 0;18471848 await act(() => {1849 const {pipe} = renderToPipeableStream(1850 <App />,18511852 {1853 onError,1854 },1855 );1856 pipe(writable);1857 });1858 expect(loggedErrors).toEqual([]);18591860 const errors = [];1861 // Attempt to hydrate the content.1862 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1863 onRecoverableError(error, errorInfo) {1864 errors.push({error, errorInfo});1865 },1866 });1867 await waitForAll([]);18681869 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18701871 await act(() => {1872 rejectComponent(theError);1873 });18741875 expect(loggedErrors).toEqual([theError]);1876 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);18771878 // Now we can client render it instead.1879 await waitForAll([]);18801881 expectErrors(1882 errors,1883 [1884 [1885 'Switched to client rendering because the server rendering errored:\n\n' +1886 theError.message,1887 expectedDigest,1888 componentStack(['Lazy', 'Suspense', 'div', 'App']),1889 ],1890 ],1891 [1892 [1893 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1894 expectedDigest,1895 ],1896 ],1897 );18981899 // The client rendered HTML is now in place.1900 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1901 expect(loggedErrors).toEqual([theError]);1902 });19031904 it('should asynchronously load the suspense boundary', async () => {1905 await act(() => {1906 const {pipe} = renderToPipeableStream(1907 <div>1908 <Suspense fallback={<Text text="Loading..." />}>1909 <AsyncText text="Hello World" />1910 </Suspense>1911 </div>,1912 );1913 pipe(writable);1914 });1915 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1916 await act(() => {1917 resolveText('Hello World');1918 });1919 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1920 });19211922 it('waits for pending content to come in from the server and then hydrates it', async () => {1923 const ref = React.createRef();19241925 function App() {1926 return (1927 <div>1928 <Suspense fallback="Loading...">1929 <h1 ref={ref}>1930 <AsyncText text="Hello" />1931 </h1>1932 </Suspense>1933 </div>1934 );1935 }19361937 let bootstrapped = false;1938 window.__INIT__ = function () {1939 bootstrapped = true;1940 // Attempt to hydrate the content.1941 ReactDOMClient.hydrateRoot(container, <App />);1942 };19431944 await act(() => {1945 const {pipe} = renderToPipeableStream(<App />, {1946 bootstrapScriptContent: '__INIT__();',1947 });1948 pipe(writable);1949 });19501951 // We're still showing a fallback.1952 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19531954 // We already bootstrapped.1955 expect(bootstrapped).toBe(true);19561957 // Attempt to hydrate the content.1958 await waitForAll([]);19591960 // We're still loading because we're waiting for the server to stream more content.1961 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);19621963 // The server now updates the content in place in the fallback.1964 await act(() => {1965 resolveText('Hello');1966 });19671968 // The final HTML is now in place.1969 expect(getVisibleChildren(container)).toEqual(1970 <div>1971 <h1>Hello</h1>1972 </div>,1973 );1974 const h1 = container.getElementsByTagName('h1')[0];19751976 // But it is not yet hydrated.1977 expect(ref.current).toBe(null);19781979 await waitForAll([]);19801981 // Now it's hydrated.1982 expect(ref.current).toBe(h1);1983 });19841985 it('handles an error on the client if the server ends up erroring', async () => {1986 const ref = React.createRef();19871988 class ErrorBoundary extends React.Component {1989 state = {error: null};1990 static getDerivedStateFromError(error) {1991 return {error};1992 }1993 render() {1994 if (this.state.error) {1995 return <b ref={ref}>{this.state.error.message}</b>;1996 }1997 return this.props.children;1998 }1999 }
Findings
✓ No findings reported for this file.