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('should asynchronously load a lazy component', async () => {410 let resolveA;411 const LazyA = React.lazy(() => {412 return new Promise(r => {413 resolveA = r;414 });415 });416417 let resolveB;418 const LazyB = React.lazy(() => {419 return new Promise(r => {420 resolveB = r;421 });422 });423424 class TextWithPunctuation extends React.Component {425 render() {426 return <Text text={this.props.text + this.props.punctuation} />;427 }428 }429430 // This tests that default props of the inner element is resolved.431 TextWithPunctuation.defaultProps = {432 punctuation: '!',433 };434435 await act(() => {436 const {pipe} = renderToPipeableStream(437 <div>438 <div>439 <Suspense fallback={<Text text="Loading..." />}>440 <LazyA text="Hello" />441 </Suspense>442 </div>443 <div>444 <Suspense fallback={<Text text="Loading..." />}>445 <LazyB text="world" />446 </Suspense>447 </div>448 </div>,449 );450 pipe(writable);451 });452453 expect(getVisibleChildren(container)).toEqual(454 <div>455 <div>Loading...</div>456 <div>Loading...</div>457 </div>,458 );459 await act(() => {460 resolveA({default: Text});461 });462 expect(getVisibleChildren(container)).toEqual(463 <div>464 <div>Hello</div>465 <div>Loading...</div>466 </div>,467 );468 await act(() => {469 resolveB({default: TextWithPunctuation});470 });471 expect(getVisibleChildren(container)).toEqual(472 <div>473 <div>Hello</div>474 <div>world!</div>475 </div>,476 );477 });478479 it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {480 const makeApp = () => {481 let resolve;482 const imports = new Promise(r => {483 resolve = () => r({default: () => <span id="async">async</span>});484 });485 const Lazy = React.lazy(() => imports);486487 const App = () => (488 <div>489 <Suspense fallback={<span>Loading...</span>}>490 <Lazy />491 <span id="after">after</span>492 </Suspense>493 </div>494 );495496 return [App, resolve];497 };498499 // Server-side500 const [App, resolve] = makeApp();501 await act(() => {502 const {pipe} = renderToPipeableStream(<App />);503 pipe(writable);504 });505 expect(getVisibleChildren(container)).toEqual(506 <div>507 <span>Loading...</span>508 </div>,509 );510 await act(() => {511 resolve();512 });513 expect(getVisibleChildren(container)).toEqual(514 <div>515 <span id="async">async</span>516 <span id="after">after</span>517 </div>,518 );519520 // Client-side521 const [HydrateApp, hydrateResolve] = makeApp();522 await act(() => {523 ReactDOMClient.hydrateRoot(container, <HydrateApp />);524 });525526 expect(getVisibleChildren(container)).toEqual(527 <div>528 <span id="async">async</span>529 <span id="after">after</span>530 </div>,531 );532533 await act(() => {534 hydrateResolve();535 });536 expect(getVisibleChildren(container)).toEqual(537 <div>538 <span id="async">async</span>539 <span id="after">after</span>540 </div>,541 );542 });543544 it('should support nonce for bootstrap and runtime scripts', async () => {545 CSPnonce = 'R4nd0m';546 try {547 let resolve;548 const Lazy = React.lazy(() => {549 return new Promise(r => {550 resolve = r;551 });552 });553554 await act(() => {555 const {pipe} = renderToPipeableStream(556 <div>557 <Suspense fallback={<Text text="Loading..." />}>558 <Lazy text="Hello" />559 </Suspense>560 </div>,561 {562 nonce: 'R4nd0m',563 bootstrapScriptContent: 'function noop(){}',564 bootstrapScripts: [565 'init.js',566 {src: 'init2.js', integrity: 'init2hash'},567 ],568 bootstrapModules: [569 'init.mjs',570 {src: 'init2.mjs', integrity: 'init2hash'},571 ],572 },573 );574 pipe(writable);575 });576577 expect(getVisibleChildren(container)).toEqual([578 <link579 rel="preload"580 fetchpriority="low"581 href="init.js"582 as="script"583 nonce={CSPnonce}584 />,585 <link586 rel="preload"587 fetchpriority="low"588 href="init2.js"589 as="script"590 nonce={CSPnonce}591 integrity="init2hash"592 />,593 <link594 rel="modulepreload"595 fetchpriority="low"596 href="init.mjs"597 nonce={CSPnonce}598 />,599 <link600 rel="modulepreload"601 fetchpriority="low"602 href="init2.mjs"603 nonce={CSPnonce}604 integrity="init2hash"605 />,606 <div>Loading...</div>,607 ]);608609 // check that there are 6 scripts with a matching nonce:610 // The runtime script or initial paint time, an inline bootstrap script, two bootstrap scripts and two bootstrap modules611 expect(612 Array.from(container.getElementsByTagName('script')).filter(613 node => node.getAttribute('nonce') === CSPnonce,614 ).length,615 ).toEqual(6);616617 await act(() => {618 resolve({default: Text});619 });620 expect(getVisibleChildren(container)).toEqual([621 <link622 rel="preload"623 fetchpriority="low"624 href="init.js"625 as="script"626 nonce={CSPnonce}627 />,628 <link629 rel="preload"630 fetchpriority="low"631 href="init2.js"632 as="script"633 nonce={CSPnonce}634 integrity="init2hash"635 />,636 <link637 rel="modulepreload"638 fetchpriority="low"639 href="init.mjs"640 nonce={CSPnonce}641 />,642 <link643 rel="modulepreload"644 fetchpriority="low"645 href="init2.mjs"646 nonce={CSPnonce}647 integrity="init2hash"648 />,649 <div>Hello</div>,650 ]);651 } finally {652 CSPnonce = null;653 }654 });655656 it('should not automatically add nonce to rendered scripts', async () => {657 CSPnonce = 'R4nd0m';658 try {659 await act(async () => {660 const {pipe} = renderToPipeableStream(661 <html>662 <body>663 <script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>664 <script nonce={CSPnonce} src="foo" async={true} />665 <script src="bar" />666 <script src="baz" integrity="qux" async={true} />667 <script type="module" src="quux" async={true} />668 <script type="module" src="corge" async={true} />669 <script670 type="module"671 src="grault"672 integrity="garply"673 async={true}674 />675 </body>676 </html>,677 {678 nonce: CSPnonce,679 },680 );681 pipe(writable);682 });683684 expect(685 stripExternalRuntimeInNodes(686 document.getElementsByTagName('script'),687 renderOptions.unstable_externalRuntimeSrc,688 ).map(n => n.outerHTML),689 ).toEqual([690 `<script nonce="${CSPnonce}" src="foo" async=""></script>`,691 `<script src="baz" integrity="qux" async=""></script>`,692 `<script type="module" src="quux" async=""></script>`,693 `<script type="module" src="corge" async=""></script>`,694 `<script type="module" src="grault" integrity="garply" async=""></script>`,695 `<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,696 `<script src="bar"></script>`,697 ]);698 } finally {699 CSPnonce = null;700 }701 });702703 it('should client render a boundary if a lazy component rejects', async () => {704 let rejectComponent;705 const promise = new Promise((resolve, reject) => {706 rejectComponent = reject;707 });708 const LazyComponent = React.lazy(() => {709 return promise;710 });711712 const LazyLazy = React.lazy(async () => {713 return {714 default: LazyComponent,715 };716 });717718 function Wrapper({children}) {719 return children;720 }721 const LazyWrapper = React.lazy(() => {722 return {723 then(callback) {724 callback({725 default: Wrapper,726 });727 },728 };729 });730731 function App({isClient}) {732 return (733 <div>734 <Suspense fallback={<Text text="Loading..." />}>735 <LazyWrapper>736 {isClient ? <Text text="Hello" /> : <LazyLazy text="Hello" />}737 </LazyWrapper>738 </Suspense>739 </div>740 );741 }742743 let bootstrapped = false;744 const errors = [];745 window.__INIT__ = function () {746 bootstrapped = true;747 // Attempt to hydrate the content.748 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {749 onRecoverableError(error, errorInfo) {750 errors.push({error, errorInfo});751 },752 });753 };754755 const theError = new Error('Test');756 const loggedErrors = [];757 function onError(x, errorInfo) {758 loggedErrors.push(x);759 return 'Hash of (' + x.message + ')';760 }761 const expectedDigest = onError(theError);762 loggedErrors.length = 0;763764 await act(() => {765 const {pipe} = renderToPipeableStream(<App isClient={false} />, {766 bootstrapScriptContent: '__INIT__();',767 onError,768 });769 pipe(writable);770 });771772 expect(loggedErrors).toEqual([]);773 expect(bootstrapped).toBe(true);774775 await waitForAll([]);776777 // We're still loading because we're waiting for the server to stream more content.778 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);779780 expect(loggedErrors).toEqual([]);781782 await act(() => {783 rejectComponent(theError);784 });785786 expect(loggedErrors).toEqual([theError]);787788 // We haven't ran the client hydration yet.789 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);790791 // Now we can client render it instead.792 await waitForAll([]);793 expectErrors(794 errors,795 [796 [797 'Switched to client rendering because the server rendering errored:\n\n' +798 theError.message,799 expectedDigest,800 componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']),801 ],802 ],803 [804 [805 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',806 expectedDigest,807 ],808 ],809 );810811 // The client rendered HTML is now in place.812 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);813814 expect(loggedErrors).toEqual([theError]);815 });816817 it('should have special stacks if Suspense fallback', async () => {818 const infinitePromise = new Promise(() => {});819 const InfiniteComponent = React.lazy(() => {820 return infinitePromise;821 });822823 function Throw({text}) {824 throw new Error(text);825 }826827 function App() {828 return (829 <Suspense fallback="Loading">830 <div>831 <Suspense fallback={<Throw text="Bye" />}>832 <InfiniteComponent text="Hi" />833 </Suspense>834 </div>835 </Suspense>836 );837 }838839 const loggedErrors = [];840 function onError(x, errorInfo) {841 loggedErrors.push({842 message: x.message,843 componentStack: errorInfo.componentStack,844 });845 return 'Hash of (' + x.message + ')';846 }847 loggedErrors.length = 0;848849 await act(() => {850 const {pipe} = renderToPipeableStream(<App />, {851 onError,852 });853 pipe(writable);854 });855856 expect(loggedErrors.length).toBe(1);857 expect(loggedErrors[0].message).toBe('Bye');858 expect(normalizeCodeLocInfo(loggedErrors[0].componentStack)).toBe(859 componentStack(['Throw', 'Suspense Fallback', 'div', 'Suspense', 'App']),860 );861 });862863 it('should asynchronously load a lazy element', async () => {864 let resolveElement;865 const lazyElement = React.lazy(() => {866 return new Promise(r => {867 resolveElement = r;868 });869 });870871 await act(() => {872 const {pipe} = renderToPipeableStream(873 <div>874 <Suspense fallback={<Text text="Loading..." />}>875 {lazyElement}876 </Suspense>877 </div>,878 );879 pipe(writable);880 });881 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);882 // Because there is no content inside the Suspense boundary that could've883 // been written, we expect to not see any additional partial data flushed884 // yet.885 expect(886 stripExternalRuntimeInNodes(887 container.childNodes,888 renderOptions.unstable_externalRuntimeSrc,889 ).length,890 ).toBe(gate(flags => flags.shouldUseFizzExternalRuntime) ? 1 : 2);891 await act(() => {892 resolveElement({default: <Text text="Hello" />});893 });894 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);895 });896897 it('should client render a boundary if a lazy element rejects', async () => {898 let rejectElement;899 const element = <Text text="Hello" />;900 const lazyElement = React.lazy(() => {901 return new Promise((resolve, reject) => {902 rejectElement = reject;903 });904 });905906 const theError = new Error('Test');907 const loggedErrors = [];908 function onError(x, errorInfo) {909 loggedErrors.push(x);910 return 'hash of (' + x.message + ')';911 }912 const expectedDigest = onError(theError);913 loggedErrors.length = 0;914915 function App({isClient}) {916 return (917 <div>918 <Suspense fallback={<Text text="Loading..." />}>919 {isClient ? element : lazyElement}920 </Suspense>921 </div>922 );923 }924925 await act(() => {926 const {pipe} = renderToPipeableStream(<App isClient={false} />, {927 onError,928 });929 pipe(writable);930 });931 expect(loggedErrors).toEqual([]);932933 const errors = [];934 // Attempt to hydrate the content.935 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {936 onRecoverableError(error, errorInfo) {937 errors.push({error, errorInfo});938 },939 });940 await waitForAll([]);941942 // We're still loading because we're waiting for the server to stream more content.943 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);944945 expect(loggedErrors).toEqual([]);946947 await act(() => {948 rejectElement(theError);949 });950951 expect(loggedErrors).toEqual([theError]);952953 // We haven't ran the client hydration yet.954 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);955956 // Now we can client render it instead.957 await waitForAll([]);958959 expectErrors(960 errors,961 [962 [963 'Switched to client rendering because the server rendering errored:\n\n' +964 theError.message,965 expectedDigest,966 componentStack(['Suspense', 'div', 'App']),967 ],968 ],969 [970 [971 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',972 expectedDigest,973 ],974 ],975 );976977 // The client rendered HTML is now in place.978 // expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);979980 expect(loggedErrors).toEqual([theError]);981 });982983 it('Errors in boundaries should be sent to the client and reported on client render - Error before flushing', async () => {984 function Indirection({level, children}) {985 if (level > 0) {986 return <Indirection level={level - 1}>{children}</Indirection>;987 }988 return children;989 }990991 const theError = new Error('uh oh');992993 function Erroring({isClient}) {994 if (isClient) {995 return 'Hello World';996 }997 throw theError;998 }9991000 function App({isClient}) {1001 return (1002 <div>1003 <Suspense fallback={<span>loading...</span>}>1004 <Indirection level={2}>1005 <Erroring isClient={isClient} />1006 </Indirection>1007 </Suspense>1008 </div>1009 );1010 }10111012 const loggedErrors = [];1013 function onError(x) {1014 loggedErrors.push(x);1015 return 'hash(' + x.message + ')';1016 }1017 const expectedDigest = onError(theError);1018 loggedErrors.length = 0;10191020 await act(() => {1021 const {pipe} = renderToPipeableStream(1022 <App />,10231024 {1025 onError,1026 },1027 );1028 pipe(writable);1029 });1030 expect(loggedErrors).toEqual([theError]);10311032 const errors = [];1033 // Attempt to hydrate the content.1034 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1035 onRecoverableError(error, errorInfo) {1036 errors.push({error, errorInfo});1037 },1038 });1039 await waitForAll([]);10401041 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);10421043 expectErrors(1044 errors,1045 [1046 [1047 'Switched to client rendering because the server rendering errored:\n\n' +1048 theError.message,1049 expectedDigest,1050 componentStack([1051 'Erroring',1052 'Indirection',1053 'Indirection',1054 'Indirection',1055 'Suspense',1056 'div',1057 'App',1058 ]),1059 ],1060 ],1061 [1062 [1063 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1064 expectedDigest,1065 ],1066 ],1067 );1068 });10691070 it('Errors in boundaries should be sent to the client and reported on client render - Error after flushing', async () => {1071 let rejectComponent;1072 const LazyComponent = React.lazy(() => {1073 return new Promise((resolve, reject) => {1074 rejectComponent = reject;1075 });1076 });10771078 function App({isClient}) {1079 return (1080 <div>1081 <Suspense fallback={<Text text="Loading..." />}>1082 {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}1083 </Suspense>1084 </div>1085 );1086 }10871088 const loggedErrors = [];1089 const theError = new Error('uh oh');1090 function onError(x) {1091 loggedErrors.push(x);1092 return 'hash(' + x.message + ')';1093 }1094 const expectedDigest = onError(theError);1095 loggedErrors.length = 0;10961097 await act(() => {1098 const {pipe} = renderToPipeableStream(1099 <App />,11001101 {1102 onError,1103 },1104 );1105 pipe(writable);1106 });1107 expect(loggedErrors).toEqual([]);11081109 const errors = [];1110 // Attempt to hydrate the content.1111 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {1112 onRecoverableError(error, errorInfo) {1113 errors.push({error, errorInfo});1114 },1115 });1116 await waitForAll([]);11171118 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);11191120 await act(() => {1121 rejectComponent(theError);1122 });11231124 expect(loggedErrors).toEqual([theError]);1125 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);11261127 // Now we can client render it instead.1128 await waitForAll([]);11291130 expectErrors(1131 errors,1132 [1133 [1134 'Switched to client rendering because the server rendering errored:\n\n' +1135 theError.message,1136 expectedDigest,1137 componentStack(['Lazy', 'Suspense', 'div', 'App']),1138 ],1139 ],1140 [1141 [1142 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1143 expectedDigest,1144 ],1145 ],1146 );11471148 // The client rendered HTML is now in place.1149 expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);1150 expect(loggedErrors).toEqual([theError]);1151 });11521153 it('should asynchronously load the suspense boundary', async () => {1154 await act(() => {1155 const {pipe} = renderToPipeableStream(1156 <div>1157 <Suspense fallback={<Text text="Loading..." />}>1158 <AsyncText text="Hello World" />1159 </Suspense>1160 </div>,1161 );1162 pipe(writable);1163 });1164 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1165 await act(() => {1166 resolveText('Hello World');1167 });1168 expect(getVisibleChildren(container)).toEqual(<div>Hello World</div>);1169 });11701171 it('waits for pending content to come in from the server and then hydrates it', async () => {1172 const ref = React.createRef();11731174 function App() {1175 return (1176 <div>1177 <Suspense fallback="Loading...">1178 <h1 ref={ref}>1179 <AsyncText text="Hello" />1180 </h1>1181 </Suspense>1182 </div>1183 );1184 }11851186 let bootstrapped = false;1187 window.__INIT__ = function () {1188 bootstrapped = true;1189 // Attempt to hydrate the content.1190 ReactDOMClient.hydrateRoot(container, <App />);1191 };11921193 await act(() => {1194 const {pipe} = renderToPipeableStream(<App />, {1195 bootstrapScriptContent: '__INIT__();',1196 });1197 pipe(writable);1198 });11991200 // We're still showing a fallback.1201 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12021203 // We already bootstrapped.1204 expect(bootstrapped).toBe(true);12051206 // Attempt to hydrate the content.1207 await waitForAll([]);12081209 // We're still loading because we're waiting for the server to stream more content.1210 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12111212 // The server now updates the content in place in the fallback.1213 await act(() => {1214 resolveText('Hello');1215 });12161217 // The final HTML is now in place.1218 expect(getVisibleChildren(container)).toEqual(1219 <div>1220 <h1>Hello</h1>1221 </div>,1222 );1223 const h1 = container.getElementsByTagName('h1')[0];12241225 // But it is not yet hydrated.1226 expect(ref.current).toBe(null);12271228 await waitForAll([]);12291230 // Now it's hydrated.1231 expect(ref.current).toBe(h1);1232 });12331234 it('handles an error on the client if the server ends up erroring', async () => {1235 const ref = React.createRef();12361237 class ErrorBoundary extends React.Component {1238 state = {error: null};1239 static getDerivedStateFromError(error) {1240 return {error};1241 }1242 render() {1243 if (this.state.error) {1244 return <b ref={ref}>{this.state.error.message}</b>;1245 }1246 return this.props.children;1247 }1248 }12491250 function App() {1251 return (1252 <ErrorBoundary>1253 <div>1254 <Suspense fallback="Loading...">1255 <span ref={ref}>1256 <AsyncText text="This Errors" />1257 </span>1258 </Suspense>1259 </div>1260 </ErrorBoundary>1261 );1262 }12631264 const loggedErrors = [];12651266 // We originally suspend the boundary and start streaming the loading state.1267 await act(() => {1268 const {pipe} = renderToPipeableStream(1269 <App />,12701271 {1272 onError(x) {1273 loggedErrors.push(x);1274 },1275 },1276 );1277 pipe(writable);1278 });12791280 // We're still showing a fallback.1281 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12821283 expect(loggedErrors).toEqual([]);12841285 // Attempt to hydrate the content.1286 ReactDOMClient.hydrateRoot(container, <App />);1287 await waitForAll([]);12881289 // We're still loading because we're waiting for the server to stream more content.1290 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);12911292 const theError = new Error('Error Message');1293 await act(() => {1294 rejectText('This Errors', theError);1295 });12961297 expect(loggedErrors).toEqual([theError]);12981299 // The server errored, but we still haven't hydrated. We don't know if the1300 // client will succeed yet, so we still show the loading state.1301 expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);1302 expect(ref.current).toBe(null);13031304 // Flush the hydration.1305 await waitForAll([]);13061307 // Hydrating should've generated an error and replaced the suspense boundary.1308 expect(getVisibleChildren(container)).toEqual(<b>Error Message</b>);13091310 const b = container.getElementsByTagName('b')[0];1311 expect(ref.current).toBe(b);1312 });13131314 // @gate enableSuspenseList1315 it('shows inserted items before pending in a SuspenseList as fallbacks while hydrating', async () => {1316 const ref = React.createRef();13171318 // These are hoisted to avoid them from rerendering.1319 const a = (1320 <Suspense fallback="Loading A">1321 <span ref={ref}>1322 <AsyncText text="A" />1323 </span>1324 </Suspense>1325 );1326 const b = (1327 <Suspense fallback="Loading B">1328 <span>1329 <Text text="B" />1330 </span>1331 </Suspense>1332 );13331334 function App({showMore}) {1335 return (1336 <div>1337 <SuspenseList revealOrder="forwards" tail="visible">1338 {a}1339 {b}1340 {showMore ? (1341 <Suspense fallback="Loading C">1342 <span>C</span>1343 </Suspense>1344 ) : null}1345 </SuspenseList>1346 </div>1347 );1348 }13491350 // We originally suspend the boundary and start streaming the loading state.1351 await act(() => {1352 const {pipe} = renderToPipeableStream(<App showMore={false} />);1353 pipe(writable);1354 });13551356 const root = ReactDOMClient.hydrateRoot(1357 container,1358 <App showMore={false} />,1359 );1360 await waitForAll([]);13611362 // We're not hydrated yet.1363 expect(ref.current).toBe(null);1364 expect(getVisibleChildren(container)).toEqual(1365 <div>1366 {'Loading A'}1367 {'Loading B'}1368 </div>,1369 );13701371 // Add more rows before we've hydrated the first two.1372 root.render(<App showMore={true} />);1373 await waitForAll([]);13741375 // We're not hydrated yet.1376 expect(ref.current).toBe(null);13771378 // We haven't resolved yet.1379 expect(getVisibleChildren(container)).toEqual(1380 <div>1381 {'Loading A'}1382 {'Loading B'}1383 {'Loading C'}1384 </div>,1385 );13861387 await act(async () => {1388 await resolveText('A');1389 });13901391 await waitForAll([]);13921393 expect(getVisibleChildren(container)).toEqual(1394 <div>1395 <span>A</span>1396 <span>B</span>1397 <span>C</span>1398 </div>,1399 );14001401 const span = container.getElementsByTagName('span')[0];1402 expect(ref.current).toBe(span);1403 });14041405 it('client renders a boundary if it does not resolve before aborting', async () => {1406 function App() {1407 return (1408 <div>1409 <Suspense fallback="Loading...">1410 <h1>1411 <AsyncText text="Hello" />1412 </h1>1413 </Suspense>1414 <main>1415 <Suspense fallback="loading...">1416 <AsyncText text="World" />1417 </Suspense>1418 </main>1419 </div>1420 );1421 }14221423 const loggedErrors = [];1424 const expectedDigest = 'Hash for Abort';1425 function onError(error) {1426 loggedErrors.push(error);1427 return expectedDigest;1428 }14291430 let controls;1431 await act(() => {1432 controls = renderToPipeableStream(<App />, {onError});1433 controls.pipe(writable);1434 });14351436 // We're still showing a fallback.14371438 const errors = [];1439 // Attempt to hydrate the content.1440 ReactDOMClient.hydrateRoot(container, <App />, {1441 onRecoverableError(error, errorInfo) {1442 errors.push({error, errorInfo});1443 },1444 });1445 await waitForAll([]);14461447 // We're still loading because we're waiting for the server to stream more content.1448 expect(getVisibleChildren(container)).toEqual(1449 <div>1450 Loading...<main>loading...</main>1451 </div>,1452 );14531454 // We abort the server response.1455 await act(() => {1456 controls.abort();1457 });14581459 // We still can't render it on the client.1460 await waitForAll([]);1461 expectErrors(1462 errors,1463 [1464 [1465 'Switched to client rendering because the server rendering aborted due to:\n\n' +1466 'The render was aborted by the server without a reason.',1467 expectedDigest,1468 // We get the stack of the task when it was aborted which is why we see `h1`1469 componentStack(['AsyncText', 'h1', 'Suspense', 'div', 'App']),1470 ],1471 [1472 'Switched to client rendering because the server rendering aborted due to:\n\n' +1473 'The render was aborted by the server without a reason.',1474 expectedDigest,1475 componentStack(['AsyncText', 'Suspense', 'main', 'div', 'App']),1476 ],1477 ],1478 [1479 [1480 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1481 expectedDigest,1482 ],1483 [1484 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1485 expectedDigest,1486 ],1487 ],1488 );1489 expect(getVisibleChildren(container)).toEqual(1490 <div>1491 Loading...<main>loading...</main>1492 </div>,1493 );14941495 // We now resolve it on the client.1496 await clientAct(() => {1497 resolveText('Hello');1498 resolveText('World');1499 });1500 assertLog([]);15011502 // The client rendered HTML is now in place.1503 expect(getVisibleChildren(container)).toEqual(1504 <div>1505 <h1>Hello</h1>1506 <main>World</main>1507 </div>,1508 );1509 });15101511 it('should allow for two containers to be written to the same document', async () => {1512 // We create two passthrough streams for each container to write into.1513 // Notably we don't implement a end() call for these. Because we don't want to1514 // close the underlying stream just because one of the streams is done. Instead1515 // we manually close when both are done.1516 const writableA = new Stream.Writable();1517 writableA._write = (chunk, encoding, next) => {1518 writable.write(chunk, encoding, next);1519 };1520 const writableB = new Stream.Writable();1521 writableB._write = (chunk, encoding, next) => {1522 writable.write(chunk, encoding, next);1523 };15241525 await act(() => {1526 const {pipe} = renderToPipeableStream(1527 // We use two nested boundaries to flush out coverage of an old reentrancy bug.1528 <div>1529 <Suspense fallback="Loading...">1530 <Suspense fallback={<Text text="Loading A..." />}>1531 <>1532 <Text text="This will show A: " />1533 <div>1534 <AsyncText text="A" />1535 </div>1536 </>1537 </Suspense>1538 </Suspense>1539 </div>,1540 {1541 identifierPrefix: 'A_',1542 onShellReady() {1543 writableA.write('<div id="container-A">');1544 pipe(writableA);1545 writableA.write('</div>');1546 },1547 },1548 );1549 });15501551 await act(() => {1552 const {pipe} = renderToPipeableStream(1553 <div>1554 <Suspense fallback={<Text text="Loading B..." />}>1555 <Text text="This will show B: " />1556 <div>1557 <AsyncText text="B" />1558 </div>1559 </Suspense>1560 </div>,1561 {1562 identifierPrefix: 'B_',1563 onShellReady() {1564 writableB.write('<div id="container-B">');1565 pipe(writableB);1566 writableB.write('</div>');1567 },1568 },1569 );1570 });15711572 expect(getVisibleChildren(container)).toEqual([1573 <div id="container-A">1574 <div>Loading A...</div>1575 </div>,1576 <div id="container-B">1577 <div>Loading B...</div>1578 </div>,1579 ]);15801581 await act(() => {1582 resolveText('B');1583 });15841585 expect(getVisibleChildren(container)).toEqual([1586 <div id="container-A">1587 <div>Loading A...</div>1588 </div>,1589 <div id="container-B">1590 <div>1591 This will show B: <div>B</div>1592 </div>1593 </div>,1594 ]);15951596 await act(() => {1597 resolveText('A');1598 });15991600 // We're done writing both streams now.1601 writable.end();16021603 expect(getVisibleChildren(container)).toEqual([1604 <div id="container-A">1605 <div>1606 This will show A: <div>A</div>1607 </div>1608 </div>,1609 <div id="container-B">1610 <div>1611 This will show B: <div>B</div>1612 </div>1613 </div>,1614 ]);1615 });16161617 it('can resolve async content in esoteric parents', async () => {1618 function AsyncOption({text}) {1619 return <option>{readText(text)}</option>;1620 }16211622 function AsyncCol({className}) {1623 return <col className={readText(className)} />;1624 }16251626 function AsyncPath({id}) {1627 return <path id={readText(id)} />;1628 }16291630 function AsyncMi({id}) {1631 return <mi id={readText(id)} />;1632 }16331634 function App() {1635 return (1636 <div>1637 <select>1638 <Suspense fallback="Loading...">1639 <AsyncOption text="Hello" />1640 </Suspense>1641 </select>1642 <Suspense fallback="Loading...">1643 <table>1644 <colgroup>1645 <AsyncCol className="World" />1646 </colgroup>1647 </table>1648 <svg>1649 <g>1650 <AsyncPath id="my-path" />1651 </g>1652 </svg>1653 <math>1654 <AsyncMi id="my-mi" />1655 </math>1656 </Suspense>1657 </div>1658 );1659 }16601661 await act(() => {1662 const {pipe} = renderToPipeableStream(<App />);1663 pipe(writable);1664 });16651666 expect(getVisibleChildren(container)).toEqual(1667 <div>1668 <select>Loading...</select>Loading...1669 </div>,1670 );16711672 await act(() => {1673 resolveText('Hello');1674 });16751676 await act(() => {1677 resolveText('World');1678 });16791680 await act(() => {1681 resolveText('my-path');1682 resolveText('my-mi');1683 });16841685 expect(getVisibleChildren(container)).toEqual(1686 <div>1687 <select>1688 <option>Hello</option>1689 </select>1690 <table>1691 <colgroup>1692 <col class="World" />1693 </colgroup>1694 </table>1695 <svg>1696 <g>1697 <path id="my-path" />1698 </g>1699 </svg>1700 <math>1701 <mi id="my-mi" />1702 </math>1703 </div>,1704 );17051706 expect(container.querySelector('#my-path').namespaceURI).toBe(1707 'http://www.w3.org/2000/svg',1708 );1709 expect(container.querySelector('#my-mi').namespaceURI).toBe(1710 'http://www.w3.org/1998/Math/MathML',1711 );1712 });17131714 it('can resolve async content in table parents', async () => {1715 function AsyncTableBody({className, children}) {1716 return <tbody className={readText(className)}>{children}</tbody>;1717 }17181719 function AsyncTableRow({className, children}) {1720 return <tr className={readText(className)}>{children}</tr>;1721 }17221723 function AsyncTableCell({text}) {1724 return <td>{readText(text)}</td>;1725 }17261727 function App() {1728 return (1729 <table>1730 <Suspense1731 fallback={1732 <tbody>1733 <tr>1734 <td>Loading...</td>1735 </tr>1736 </tbody>1737 }>1738 <AsyncTableBody className="A">1739 <AsyncTableRow className="B">1740 <AsyncTableCell text="C" />1741 </AsyncTableRow>1742 </AsyncTableBody>1743 </Suspense>1744 </table>1745 );1746 }17471748 await act(() => {1749 const {pipe} = renderToPipeableStream(<App />);1750 pipe(writable);1751 });17521753 expect(getVisibleChildren(container)).toEqual(1754 <table>1755 <tbody>1756 <tr>1757 <td>Loading...</td>1758 </tr>1759 </tbody>1760 </table>,1761 );17621763 await act(() => {1764 resolveText('A');1765 });17661767 await act(() => {1768 resolveText('B');1769 });17701771 await act(() => {1772 resolveText('C');1773 });17741775 expect(getVisibleChildren(container)).toEqual(1776 <table>1777 <tbody class="A">1778 <tr class="B">1779 <td>C</td>1780 </tr>1781 </tbody>1782 </table>,1783 );1784 });17851786 it('can stream into an SVG container', async () => {1787 function AsyncPath({id}) {1788 return <path id={readText(id)} />;1789 }17901791 function App() {1792 return (1793 <g>1794 <Suspense fallback={<text>Loading...</text>}>1795 <AsyncPath id="my-path" />1796 </Suspense>1797 </g>1798 );1799 }18001801 await act(() => {1802 const {pipe} = renderToPipeableStream(1803 <App />,18041805 {1806 namespaceURI: 'http://www.w3.org/2000/svg',1807 onShellReady() {1808 writable.write('<svg>');1809 pipe(writable);1810 writable.write('</svg>');1811 },1812 },1813 );1814 });18151816 expect(getVisibleChildren(container)).toEqual(1817 <svg>1818 <g>1819 <text>Loading...</text>1820 </g>1821 </svg>,1822 );18231824 await act(() => {1825 resolveText('my-path');1826 });18271828 expect(getVisibleChildren(container)).toEqual(1829 <svg>1830 <g>1831 <path id="my-path" />1832 </g>1833 </svg>,1834 );18351836 expect(container.querySelector('#my-path').namespaceURI).toBe(1837 'http://www.w3.org/2000/svg',1838 );1839 });18401841 function normalizeCodeLocInfo(str) {1842 return (1843 str &&1844 String(str).replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {1845 return '\n in ' + name + ' (at **)';1846 })1847 );1848 }18491850 it('should include a component stack across suspended boundaries', async () => {1851 function B() {1852 const children = [readText('Hello'), readText('World')];1853 // Intentionally trigger a key warning here.1854 return (1855 <div>1856 {children.map(function mapper(t) {1857 return <span>{t}</span>;1858 })}1859 </div>1860 );1861 }1862 function C() {1863 return (1864 <inCorrectTag>1865 <Text text="Loading" />1866 </inCorrectTag>1867 );1868 }1869 function A() {1870 return (1871 <div>1872 <Suspense fallback={<C />}>1873 <B />1874 </Suspense>1875 </div>1876 );1877 }18781879 await act(() => {1880 const {pipe} = renderToPipeableStream(<A />);1881 pipe(writable);1882 });18831884 expect(getVisibleChildren(container)).toEqual(1885 <div>1886 <incorrecttag>Loading</incorrecttag>1887 </div>,1888 );18891890 assertConsoleErrorDev([1891 '<inCorrectTag /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.' +1892 '\n' +1893 ' in inCorrectTag (at **)\n' +1894 ' in C (at **)\n' +1895 ' in A (at **)',1896 ]);18971898 await act(() => {1899 resolveText('Hello');1900 resolveText('World');1901 });19021903 assertConsoleErrorDev([1904 'Each child in a list should have a unique "key" prop.\n\nCheck the render method of `B`.' +1905 ' See https://react.dev/link/warning-keys for more information.\n' +1906 ' in span (at **)\n' +1907 ' in mapper (at **)\n' +1908 ' in Array.map (at **)\n' +1909 ' in B (at **)\n' +1910 ' in A (at **)',1911 ]);19121913 expect(getVisibleChildren(container)).toEqual(1914 <div>1915 <div>1916 <span>Hello</span>1917 <span>World</span>1918 </div>1919 </div>,1920 );1921 });19221923 // @gate !disableLegacyContext1924 it('should can suspend in a class component with legacy context', async () => {1925 class TestProvider extends React.Component {1926 static childContextTypes = {1927 test: PropTypes.string,1928 };1929 state = {ctxToSet: null};1930 static getDerivedStateFromProps(props, state) {1931 return {ctxToSet: props.ctx};1932 }1933 getChildContext() {1934 return {1935 test: this.state.ctxToSet,1936 };1937 }1938 render() {1939 return this.props.children;1940 }1941 }19421943 class TestConsumer extends React.Component {1944 static contextTypes = {1945 test: PropTypes.string,1946 };1947 render() {1948 const child = (1949 <b>1950 <Text text={this.context.test} />1951 </b>1952 );1953 if (this.props.prefix) {1954 return (1955 <>1956 {readText(this.props.prefix)}1957 {child}1958 </>1959 );1960 }1961 return child;1962 }1963 }19641965 await act(() => {1966 const {pipe} = renderToPipeableStream(1967 <TestProvider ctx="A">1968 <div>1969 <Suspense1970 fallback={1971 <>1972 <Text text="Loading: " />1973 <TestConsumer />1974 </>1975 }>1976 <TestProvider ctx="B">1977 <TestConsumer prefix="Hello: " />1978 </TestProvider>1979 <TestConsumer />1980 </Suspense>1981 </div>1982 </TestProvider>,1983 );1984 pipe(writable);1985 });1986 assertConsoleErrorDev([1987 'TestProvider uses the legacy childContextTypes API which will soon be removed. ' +1988 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +1989 ' in TestProvider (at **)',1990 'TestConsumer uses the legacy contextTypes API which will soon be removed. ' +1991 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +1992 ' in TestConsumer (at **)',1993 ]);1994 expect(getVisibleChildren(container)).toEqual(1995 <div>1996 Loading: <b>A</b>1997 </div>,1998 );1999 await act(() => {2000 resolveText('Hello: ');
Findings
✓ No findings reported for this file.