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} from '../test-utils/FizzTestUtils';1617let JSDOM;18let Stream;19let React;20let ReactDOM;21let ReactDOMClient;22let ReactDOMFizzServer;23let Suspense;24let SuspenseList;25let textCache;26let loadCache;27let writable;28let CSPnonce = null;29let container;30let buffer = '';31let hasErrored = false;32let fatalError = undefined;33let renderOptions;34let waitForAll;35let assertLog;36let Scheduler;37let clientAct;38let streamingContainer;39let assertConsoleErrorDev;4041describe('ReactDOMFloat', () => {42 beforeEach(() => {43 jest.resetModules();44 JSDOM = require('jsdom').JSDOM;4546 const jsdom = new JSDOM(47 '<!DOCTYPE html><html><head></head><body><div id="container">',48 {49 runScripts: 'dangerously',50 },51 );52 // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else53 Object.defineProperty(jsdom.window, 'matchMedia', {54 writable: true,55 value: jest.fn().mockImplementation(query => ({56 matches: query === 'all' || query === '',57 media: query,58 })),59 });60 streamingContainer = null;61 global.window = jsdom.window;62 global.document = global.window.document;63 global.navigator = global.window.navigator;64 global.Node = global.window.Node;65 global.addEventListener = global.window.addEventListener;66 global.MutationObserver = global.window.MutationObserver;67 // The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.68 global.requestAnimationFrame = global.window.requestAnimationFrame = cb =>69 setTimeout(cb);70 container = document.getElementById('container');7172 CSPnonce = null;73 React = require('react');74 ReactDOM = require('react-dom');75 ReactDOMClient = require('react-dom/client');76 ReactDOMFizzServer = require('react-dom/server');77 Stream = require('stream');78 Suspense = React.Suspense;79 SuspenseList = React.unstable_SuspenseList;80 Scheduler = require('scheduler/unstable_mock');8182 const InternalTestUtils = require('internal-test-utils');83 waitForAll = InternalTestUtils.waitForAll;84 assertLog = InternalTestUtils.assertLog;85 clientAct = InternalTestUtils.act;86 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;8788 textCache = new Map();89 loadCache = new Set();9091 buffer = '';92 hasErrored = false;9394 writable = new Stream.PassThrough();95 writable.setEncoding('utf8');96 writable.on('data', chunk => {97 buffer += chunk;98 });99 writable.on('error', error => {100 hasErrored = true;101 fatalError = error;102 });103104 renderOptions = {};105 if (gate(flags => flags.shouldUseFizzExternalRuntime)) {106 renderOptions.unstable_externalRuntimeSrc =107 'react-dom/unstable_server-external-runtime';108 }109 });110111 const bodyStartMatch = /<body(?:>| .*?>)/;112 const headStartMatch = /<head(?:>| .*?>)/;113114 async function act(callback) {115 await callback();116 // Await one turn around the event loop.117 // This assumes that we'll flush everything we have so far.118 await new Promise(resolve => {119 setImmediate(resolve);120 });121 if (hasErrored) {122 throw fatalError;123 }124 // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.125 // We also want to execute any scripts that are embedded.126 // We assume that we have now received a proper fragment of HTML.127 let bufferedContent = buffer;128 buffer = '';129130 if (!bufferedContent) {131 jest.runAllTimers();132 return;133 }134135 const bodyMatch = bufferedContent.match(bodyStartMatch);136 const headMatch = bufferedContent.match(headStartMatch);137138 if (streamingContainer === null) {139 // This is the first streamed content. We decide here where to insert it. If we get <html>, <head>, or <body>140 // we abandon the pre-built document and start from scratch. If we get anything else we assume it goes into the141 // container. This is not really production behavior because you can't correctly stream into a deep div effectively142 // but it's pragmatic for tests.143144 if (145 bufferedContent.startsWith('<head>') ||146 bufferedContent.startsWith('<head ') ||147 bufferedContent.startsWith('<body>') ||148 bufferedContent.startsWith('<body ')149 ) {150 // wrap in doctype to normalize the parsing process151 bufferedContent = '<!DOCTYPE html><html>' + bufferedContent;152 } else if (153 bufferedContent.startsWith('<html>') ||154 bufferedContent.startsWith('<html ')155 ) {156 throw new Error(157 'Recieved <html> without a <!DOCTYPE html> which is almost certainly a bug in React',158 );159 }160161 if (bufferedContent.startsWith('<!DOCTYPE html>')) {162 // we can just use the whole document163 const tempDom = new JSDOM(bufferedContent);164165 // Wipe existing head and body content166 document.head.innerHTML = '';167 document.body.innerHTML = '';168169 // Copy the <html> attributes over170 const tempHtmlNode = tempDom.window.document.documentElement;171 for (let i = 0; i < tempHtmlNode.attributes.length; i++) {172 const attr = tempHtmlNode.attributes[i];173 document.documentElement.setAttribute(attr.name, attr.value);174 }175176 if (headMatch) {177 // We parsed a head open tag. we need to copy head attributes and insert future178 // content into <head>179 streamingContainer = document.head;180 const tempHeadNode = tempDom.window.document.head;181 for (let i = 0; i < tempHeadNode.attributes.length; i++) {182 const attr = tempHeadNode.attributes[i];183 document.head.setAttribute(attr.name, attr.value);184 }185 const source = document.createElement('head');186 source.innerHTML = tempHeadNode.innerHTML;187 await insertNodesAndExecuteScripts(source, document.head, CSPnonce);188 }189190 if (bodyMatch) {191 // We parsed a body open tag. we need to copy head attributes and insert future192 // content into <body>193 streamingContainer = document.body;194 const tempBodyNode = tempDom.window.document.body;195 for (let i = 0; i < tempBodyNode.attributes.length; i++) {196 const attr = tempBodyNode.attributes[i];197 document.body.setAttribute(attr.name, attr.value);198 }199 const source = document.createElement('body');200 source.innerHTML = tempBodyNode.innerHTML;201 await insertNodesAndExecuteScripts(source, document.body, CSPnonce);202 }203204 if (!headMatch && !bodyMatch) {205 throw new Error('expected <head> or <body> after <html>');206 }207 } else {208 // we assume we are streaming into the default container'209 streamingContainer = container;210 const div = document.createElement('div');211 div.innerHTML = bufferedContent;212 await insertNodesAndExecuteScripts(div, container, CSPnonce);213 }214 } else if (streamingContainer === document.head) {215 bufferedContent = '<!DOCTYPE html><html><head>' + bufferedContent;216 const tempDom = new JSDOM(bufferedContent);217218 const tempHeadNode = tempDom.window.document.head;219 const source = document.createElement('head');220 source.innerHTML = tempHeadNode.innerHTML;221 await insertNodesAndExecuteScripts(source, document.head, CSPnonce);222223 if (bodyMatch) {224 streamingContainer = document.body;225226 const tempBodyNode = tempDom.window.document.body;227 for (let i = 0; i < tempBodyNode.attributes.length; i++) {228 const attr = tempBodyNode.attributes[i];229 document.body.setAttribute(attr.name, attr.value);230 }231 const bodySource = document.createElement('body');232 bodySource.innerHTML = tempBodyNode.innerHTML;233 await insertNodesAndExecuteScripts(bodySource, document.body, CSPnonce);234 }235 } else {236 const div = document.createElement('div');237 div.innerHTML = bufferedContent;238 await insertNodesAndExecuteScripts(div, streamingContainer, CSPnonce);239 }240 await 0;241 // Let throttled boundaries reveal242 jest.runAllTimers();243 }244245 function getMeaningfulChildren(element) {246 const children = [];247 let node = element.firstChild;248 while (node) {249 if (node.nodeType === 1) {250 if (251 // some tags are ambiguous and might be hidden because they look like non-meaningful children252 // so we have a global override where if this data attribute is included we also include the node253 node.hasAttribute('data-meaningful') ||254 (node.tagName === 'SCRIPT' &&255 node.hasAttribute('src') &&256 node.getAttribute('src') !==257 renderOptions.unstable_externalRuntimeSrc &&258 node.hasAttribute('async')) ||259 (node.tagName !== 'SCRIPT' &&260 node.tagName !== 'TEMPLATE' &&261 node.tagName !== 'template' &&262 !node.hasAttribute('hidden') &&263 !node.hasAttribute('aria-hidden') &&264 // Ignore the render blocking expect265 (node.getAttribute('rel') !== 'expect' ||266 node.getAttribute('blocking') !== 'render'))267 ) {268 const props = {};269 const attributes = node.attributes;270 for (let i = 0; i < attributes.length; i++) {271 if (272 attributes[i].name === 'id' &&273 attributes[i].value.includes(':')274 ) {275 // We assume this is a React added ID that's a non-visual implementation detail.276 continue;277 }278 props[attributes[i].name] = attributes[i].value;279 }280 props.children = getMeaningfulChildren(node);281 children.push(React.createElement(node.tagName.toLowerCase(), props));282 }283 } else if (node.nodeType === 3) {284 children.push(node.data);285 }286 node = node.nextSibling;287 }288 return children.length === 0289 ? undefined290 : children.length === 1291 ? children[0]292 : children;293 }294295 function BlockedOn({value, children}) {296 readText(value);297 return children;298 }299300 function resolveText(text) {301 const record = textCache.get(text);302 if (record === undefined) {303 const newRecord = {304 status: 'resolved',305 value: text,306 };307 textCache.set(text, newRecord);308 } else if (record.status === 'pending') {309 const thenable = record.value;310 record.status = 'resolved';311 record.value = text;312 thenable.pings.forEach(t => t());313 }314 }315316 function readText(text) {317 const record = textCache.get(text);318 if (record !== undefined) {319 switch (record.status) {320 case 'pending':321 throw record.value;322 case 'rejected':323 throw record.value;324 case 'resolved':325 return record.value;326 }327 } else {328 const thenable = {329 pings: [],330 then(resolve) {331 if (newRecord.status === 'pending') {332 thenable.pings.push(resolve);333 } else {334 Promise.resolve().then(() => resolve(newRecord.value));335 }336 },337 };338339 const newRecord = {340 status: 'pending',341 value: thenable,342 };343 textCache.set(text, newRecord);344345 throw thenable;346 }347 }348349 function AsyncText({text}) {350 return readText(text);351 }352353 function renderToPipeableStream(jsx, options) {354 // Merge options with renderOptions, which may contain featureFlag specific behavior355 return ReactDOMFizzServer.renderToPipeableStream(356 jsx,357 mergeOptions(options, renderOptions),358 );359 }360361 function loadPreloads(hrefs) {362 const event = new window.Event('load');363 const nodes = document.querySelectorAll('link[rel="preload"]');364 resolveLoadables(hrefs, nodes, event, href =>365 Scheduler.log('load preload: ' + href),366 );367 }368369 function errorPreloads(hrefs) {370 const event = new window.Event('error');371 const nodes = document.querySelectorAll('link[rel="preload"]');372 resolveLoadables(hrefs, nodes, event, href =>373 Scheduler.log('error preload: ' + href),374 );375 }376377 function loadStylesheets(hrefs) {378 loadStylesheetsFrom(document, hrefs);379 }380381 function loadStylesheetsFrom(root, hrefs) {382 const event = new window.Event('load');383 const nodes = root.querySelectorAll('link[rel="stylesheet"]');384 resolveLoadables(hrefs, nodes, event, href => {385 Scheduler.log('load stylesheet: ' + href);386 });387 }388389 function errorStylesheets(hrefs) {390 const event = new window.Event('error');391 const nodes = document.querySelectorAll('link[rel="stylesheet"]');392 resolveLoadables(hrefs, nodes, event, href => {393 Scheduler.log('error stylesheet: ' + href);394 });395 }396397 function resolveLoadables(hrefs, nodes, event, onLoad) {398 const hrefSet = hrefs ? new Set(hrefs) : null;399 for (let i = 0; i < nodes.length; i++) {400 const node = nodes[i];401 if (loadCache.has(node)) {402 continue;403 }404 const href = node.getAttribute('href');405 if (!hrefSet || hrefSet.has(href)) {406 loadCache.add(node);407 onLoad(href);408 node.dispatchEvent(event);409 }410 }411 }412413 it('can render resources before singletons', async () => {414 const root = ReactDOMClient.createRoot(document);415 root.render(416 <>417 <title>foo</title>418 <html>419 <head>420 <link rel="foo" href="foo" />421 </head>422 <body>hello world</body>423 </html>424 </>,425 );426 try {427 await waitForAll([]);428 } catch (e) {429 // for DOMExceptions that happen when expecting this test to fail we need430 // to clear the scheduler first otherwise the expected failure will fail431 await waitForAll([]);432 throw e;433 }434 expect(getMeaningfulChildren(document)).toEqual(435 <html>436 <head>437 <title>foo</title>438 <link rel="foo" href="foo" />439 </head>440 <body>hello world</body>441 </html>,442 );443 });444445 it('can hydrate non Resources in head when Resources are also inserted there', async () => {446 await act(() => {447 const {pipe} = renderToPipeableStream(448 <html>449 <head>450 <meta property="foo" content="bar" />451 <link rel="foo" href="bar" onLoad={() => {}} />452 <title>foo</title>453 <noscript>454 <link rel="icon" href="icon" />455 </noscript>456 <base target="foo" href="bar" />457 <script async={true} src="foo" onLoad={() => {}} />458 </head>459 <body>foo</body>460 </html>,461 );462 pipe(writable);463 });464 expect(getMeaningfulChildren(document)).toEqual(465 <html>466 <head>467 <meta property="foo" content="bar" />468 <title>foo</title>469 <link rel="foo" href="bar" />470 <noscript><link rel="icon" href="icon"></noscript>471 <base target="foo" href="bar" />472 <script async="" src="foo" />473 </head>474 <body>foo</body>475 </html>,476 );477478 ReactDOMClient.hydrateRoot(479 document,480 <html>481 <head>482 <meta property="foo" content="bar" />483 <link rel="foo" href="bar" onLoad={() => {}} />484 <title>foo</title>485 <noscript>486 <link rel="icon" href="icon" />487 </noscript>488 <base target="foo" href="bar" />489 <script async={true} src="foo" onLoad={() => {}} />490 </head>491 <body>foo</body>492 </html>,493 );494 await waitForAll([]);495 expect(getMeaningfulChildren(document)).toEqual(496 <html>497 <head>498 <meta property="foo" content="bar" />499 <title>foo</title>500 <link rel="foo" href="bar" />501 <noscript><link rel="icon" href="icon"></noscript>502 <base target="foo" href="bar" />503 <script async="" src="foo" />504 </head>505 <body>foo</body>506 </html>,507 );508 });509510 it('warns if you render resource-like elements above <head> or <body>', async () => {511 const root = ReactDOMClient.createRoot(document);512513 root.render(514 <>515 <noscript>foo</noscript>516 <html>517 <body>foo</body>518 </html>519 </>,520 );521 await waitForAll([]);522 assertConsoleErrorDev([523 'Cannot render <noscript> outside the main document. Try moving it into the root <head> tag.',524 ]);525526 root.render(527 <html>528 <template>foo</template>529 <body>foo</body>530 </html>,531 );532 await waitForAll([]);533 assertConsoleErrorDev([534 'Cannot render <template> outside the main document. Try moving it into the root <head> tag.\n' +535 ' in html (at **)',536 'In HTML, <template> cannot be a child of <html>.\n' +537 'This will cause a hydration error.\n\n' +538 '> <html>\n' +539 '> <template>\n' +540 ' ...\n' +541 '\n' +542 ' in template (at **)',543 ]);544545 root.render(546 <html>547 <body>foo</body>548 <style>foo</style>549 </html>,550 );551 await waitForAll([]);552 assertConsoleErrorDev([553 'Cannot render a <style> outside the main document without knowing its precedence ' +554 'and a unique href key. React can hoist and deduplicate <style> tags if you provide a ' +555 '`precedence` prop along with an `href` prop that does not conflict with the `href` ' +556 'values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +557 'Note that hoisting <style> tags is considered an advanced feature that most will not use directly. ' +558 'Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` ' +559 'and `href="some unique resource identifier"`.\n' +560 ' in html (at **)',561 'In HTML, <style> cannot be a child of <html>.\n' +562 'This will cause a hydration error.\n\n' +563 '> <html>\n' +564 ' <body>\n' +565 '> <style>\n' +566 '\n' +567 ' in style (at **)',568 ]);569570 root.render(571 <>572 <html>573 <body>foo</body>574 </html>575 <link rel="stylesheet" href="foo" />576 </>,577 );578 await waitForAll([]);579 assertConsoleErrorDev([580 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence. ' +581 'Consider adding precedence="default" or moving it into the root <head> tag.',582 ]);583584 root.render(585 <>586 <html>587 <body>foo</body>588 <script href="foo" />589 </html>590 </>,591 );592 await waitForAll([]);593 assertConsoleErrorDev([594 'Cannot render a sync or defer <script> outside the main document without knowing its order. ' +595 'Try adding async="" or moving it into the root <head> tag.\n' +596 ' in html (at **)',597 'In HTML, <script> cannot be a child of <html>.\n' +598 'This will cause a hydration error.\n' +599 '\n' +600 '> <html>\n' +601 ' <body>\n' +602 '> <script href="foo">\n' +603 '\n' +604 ' in script (at **)',605 'Encountered a script tag while rendering React component. ' +606 'Scripts inside React components are never executed when rendering on the client. ' +607 'Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +608 ' in script (at **)',609 ]);610611 root.render(612 <html>613 <script async={true} onLoad={() => {}} href="bar" />614 <body>foo</body>615 </html>,616 );617 await waitForAll([]);618 assertConsoleErrorDev([619 'Cannot render a <script> with onLoad or onError listeners outside the main document. ' +620 'Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or ' +621 'somewhere in the <body>.\n' +622 ' in html (at **)',623 ]);624625 root.render(626 <>627 <link rel="foo" onLoad={() => {}} href="bar" />628 <html>629 <body>foo</body>630 </html>631 </>,632 );633 await waitForAll([]);634 assertConsoleErrorDev([635 'Cannot render a <link> with onLoad or onError listeners outside the main document. ' +636 'Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or ' +637 'somewhere in the <body>.',638 ]);639 return;640 });641642 it('can acquire a resource after releasing it in the same commit', async () => {643 const root = ReactDOMClient.createRoot(container);644 root.render(645 <>646 <script async={true} src="foo" />647 </>,648 );649 await waitForAll([]);650 expect(getMeaningfulChildren(document)).toEqual(651 <html>652 <head>653 <script async="" src="foo" />654 </head>655 <body>656 <div id="container" />657 </body>658 </html>,659 );660661 root.render(662 <>663 {null}664 <script data-new="new" async={true} src="foo" />665 </>,666 );667 await waitForAll([]);668 // we don't see the attribute because the resource is the same and was not reconstructed669 expect(getMeaningfulChildren(document)).toEqual(670 <html>671 <head>672 <script async="" src="foo" />673 </head>674 <body>675 <div id="container" />676 </body>677 </html>,678 );679 });680681 it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {682 const chunks = [];683684 writable.on('data', chunk => {685 chunks.push(chunk);686 });687688 await act(() => {689 const {pipe} = renderToPipeableStream(690 <>691 <title>foo</title>692 <html>693 <body>bar</body>694 </html>695 <script async={true} src="foo" />696 </>,697 );698 pipe(writable);699 });700 expect(chunks).toEqual([701 '<!DOCTYPE html><html><head><script async="" src="foo"></script>' +702 (gate(flags => flags.shouldUseFizzExternalRuntime)703 ? '<script src="react-dom/unstable_server-external-runtime" async=""></script>'704 : '') +705 (gate(flags => flags.enableFizzBlockingRender)706 ? '<link rel="expect" href="#_R_" blocking="render"/>'707 : '') +708 '<title>foo</title></head>' +709 '<body>bar' +710 (gate(flags => flags.enableFizzBlockingRender)711 ? '<template id="_R_"></template>'712 : ''),713 '</body></html>',714 ]);715 });716717 it('dedupes if the external runtime is explicitly loaded using preinit', async () => {718 const unstable_externalRuntimeSrc = 'src-of-external-runtime';719 function App() {720 ReactDOM.preinit(unstable_externalRuntimeSrc, {as: 'script'});721 return (722 <div>723 <Suspense fallback={<h1>Loading...</h1>}>724 <AsyncText text="Hello" />725 </Suspense>726 </div>727 );728 }729730 await act(() => {731 const {pipe} = renderToPipeableStream(732 <html>733 <head />734 <body>735 <App />736 </body>737 </html>,738 {739 unstable_externalRuntimeSrc,740 },741 );742 pipe(writable);743 });744745 expect(746 Array.from(document.querySelectorAll('script[async]')).map(747 n => n.outerHTML,748 ),749 ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);750 });751752 it('can send style insertion implementation independent of boundary commpletion instruction implementation', async () => {753 await act(() => {754 renderToPipeableStream(755 <html>756 <body>757 <Suspense fallback="loading foo...">758 <BlockedOn value="foo">foo</BlockedOn>759 </Suspense>760 <Suspense fallback="loading bar...">761 <BlockedOn value="bar">762 <link rel="stylesheet" href="bar" precedence="bar" />763 bar764 </BlockedOn>765 </Suspense>766 </body>767 </html>,768 ).pipe(writable);769 });770771 expect(getMeaningfulChildren(document)).toEqual(772 <html>773 <head />774 <body>775 {'loading foo...'}776 {'loading bar...'}777 </body>778 </html>,779 );780781 await act(() => {782 resolveText('foo');783 });784 expect(getMeaningfulChildren(document)).toEqual(785 <html>786 <head />787 <body>788 foo789 {'loading bar...'}790 </body>791 </html>,792 );793 await act(() => {794 resolveText('bar');795 });796 expect(getMeaningfulChildren(document)).toEqual(797 <html>798 <head>799 <link rel="stylesheet" href="bar" data-precedence="bar" />800 </head>801 <body>802 foo803 {'loading bar...'}804 <link rel="preload" href="bar" as="style" />805 </body>806 </html>,807 );808 });809810 it('can avoid inserting a late stylesheet if it already rendered on the client', async () => {811 await act(() => {812 renderToPipeableStream(813 <html>814 <body>815 <Suspense fallback="loading foo...">816 <BlockedOn value="foo">817 <link rel="stylesheet" href="foo" precedence="foo" />818 foo819 </BlockedOn>820 </Suspense>821 <Suspense fallback="loading bar...">822 <BlockedOn value="bar">823 <link rel="stylesheet" href="bar" precedence="bar" />824 bar825 </BlockedOn>826 </Suspense>827 </body>828 </html>,829 ).pipe(writable);830 });831832 expect(getMeaningfulChildren(document)).toEqual(833 <html>834 <head />835 <body>836 {'loading foo...'}837 {'loading bar...'}838 </body>839 </html>,840 );841842 ReactDOMClient.hydrateRoot(843 document,844 <html>845 <body>846 <link rel="stylesheet" href="foo" precedence="foo" />847 <Suspense fallback="loading foo...">848 <link rel="stylesheet" href="foo" precedence="foo" />849 foo850 </Suspense>851 <Suspense fallback="loading bar...">852 <link rel="stylesheet" href="bar" precedence="bar" />853 bar854 </Suspense>855 </body>856 </html>,857 );858 await waitForAll([]);859 loadPreloads();860 await assertLog(['load preload: foo']);861 expect(getMeaningfulChildren(document)).toEqual(862 <html>863 <head>864 <link rel="stylesheet" href="foo" data-precedence="foo" />865 <link as="style" href="foo" rel="preload" />866 </head>867 <body>868 {'loading foo...'}869 {'loading bar...'}870 </body>871 </html>,872 );873874 await act(() => {875 resolveText('bar');876 });877 await act(() => {878 loadStylesheets();879 });880 await assertLog(['load stylesheet: foo', 'load stylesheet: bar']);881 expect(getMeaningfulChildren(document)).toEqual(882 <html>883 <head>884 <link rel="stylesheet" href="foo" data-precedence="foo" />885 <link rel="stylesheet" href="bar" data-precedence="bar" />886 <link as="style" href="foo" rel="preload" />887 </head>888 <body>889 {'loading foo...'}890 {'bar'}891 <link as="style" href="bar" rel="preload" />892 </body>893 </html>,894 );895896 await act(() => {897 resolveText('foo');898 });899 await act(() => {900 loadStylesheets();901 });902 await assertLog([]);903 expect(getMeaningfulChildren(document)).toEqual(904 <html>905 <head>906 <link rel="stylesheet" href="foo" data-precedence="foo" />907 <link rel="stylesheet" href="bar" data-precedence="bar" />908 <link as="style" href="foo" rel="preload" />909 </head>910 <body>911 {'foo'}912 {'bar'}913 <link as="style" href="bar" rel="preload" />914 <link as="style" href="foo" rel="preload" />915 </body>916 </html>,917 );918 });919920 it('can hoist <link rel="stylesheet" .../> and <style /> tags together, respecting order of discovery', async () => {921 const css = `922body {923 background-color: red;924}`;925926 await act(() => {927 renderToPipeableStream(928 <html>929 <body>930 <link rel="stylesheet" href="one1" precedence="one" />931 <style href="two1" precedence="two">932 {css}933 </style>934 <link rel="stylesheet" href="three1" precedence="three" />935 <style href="four1" precedence="four">936 {css}937 </style>938 <Suspense>939 <BlockedOn value="block">940 <link rel="stylesheet" href="one2" precedence="one" />941 <link rel="stylesheet" href="two2" precedence="two" />942 <style href="three2" precedence="three">943 {css}944 </style>945 <style href="four2" precedence="four">946 {css}947 </style>948 <link rel="stylesheet" href="five1" precedence="five" />949 </BlockedOn>950 </Suspense>951 <Suspense>952 <BlockedOn value="block2">953 <style href="one3" precedence="one">954 {css}955 </style>956 <style href="two3" precedence="two">957 {css}958 </style>959 <link rel="stylesheet" href="three3" precedence="three" />960 <link rel="stylesheet" href="four3" precedence="four" />961 <style href="six1" precedence="six">962 {css}963 </style>964 </BlockedOn>965 </Suspense>966 <Suspense>967 <BlockedOn value="block again">968 <link rel="stylesheet" href="one2" precedence="one" />969 <link rel="stylesheet" href="two2" precedence="two" />970 <style href="three2" precedence="three">971 {css}972 </style>973 <style href="four2" precedence="four">974 {css}975 </style>976 <link rel="stylesheet" href="five1" precedence="five" />977 </BlockedOn>978 </Suspense>979 </body>980 </html>,981 ).pipe(writable);982 });983984 expect(getMeaningfulChildren(document)).toEqual(985 <html>986 <head>987 <link rel="stylesheet" href="one1" data-precedence="one" />988 <style data-href="two1" data-precedence="two">989 {css}990 </style>991 <link rel="stylesheet" href="three1" data-precedence="three" />992 <style data-href="four1" data-precedence="four">993 {css}994 </style>995 </head>996 <body />997 </html>,998 );9991000 await act(() => {1001 resolveText('block');1002 });10031004 expect(getMeaningfulChildren(document)).toEqual(1005 <html>1006 <head>1007 <link rel="stylesheet" href="one1" data-precedence="one" />1008 <link rel="stylesheet" href="one2" data-precedence="one" />1009 <style data-href="two1" data-precedence="two">1010 {css}1011 </style>1012 <link rel="stylesheet" href="two2" data-precedence="two" />1013 <link rel="stylesheet" href="three1" data-precedence="three" />1014 <style data-href="three2" data-precedence="three">1015 {css}1016 </style>1017 <style data-href="four1" data-precedence="four">1018 {css}1019 </style>1020 <style data-href="four2" data-precedence="four">1021 {css}1022 </style>1023 <link rel="stylesheet" href="five1" data-precedence="five" />1024 </head>1025 <body>1026 <link rel="preload" href="one2" as="style" />1027 <link rel="preload" href="two2" as="style" />1028 <link rel="preload" href="five1" as="style" />1029 </body>1030 </html>,1031 );10321033 await act(() => {1034 resolveText('block2');1035 });10361037 expect(getMeaningfulChildren(document)).toEqual(1038 <html>1039 <head>1040 <link rel="stylesheet" href="one1" data-precedence="one" />1041 <link rel="stylesheet" href="one2" data-precedence="one" />1042 <style data-href="one3" data-precedence="one">1043 {css}1044 </style>1045 <style data-href="two1" data-precedence="two">1046 {css}1047 </style>1048 <link rel="stylesheet" href="two2" data-precedence="two" />1049 <style data-href="two3" data-precedence="two">1050 {css}1051 </style>1052 <link rel="stylesheet" href="three1" data-precedence="three" />1053 <style data-href="three2" data-precedence="three">1054 {css}1055 </style>1056 <link rel="stylesheet" href="three3" data-precedence="three" />1057 <style data-href="four1" data-precedence="four">1058 {css}1059 </style>1060 <style data-href="four2" data-precedence="four">1061 {css}1062 </style>1063 <link rel="stylesheet" href="four3" data-precedence="four" />1064 <link rel="stylesheet" href="five1" data-precedence="five" />1065 <style data-href="six1" data-precedence="six">1066 {css}1067 </style>1068 </head>1069 <body>1070 <link rel="preload" href="one2" as="style" />1071 <link rel="preload" href="two2" as="style" />1072 <link rel="preload" href="five1" as="style" />1073 <link rel="preload" href="three3" as="style" />1074 <link rel="preload" href="four3" as="style" />1075 </body>1076 </html>,1077 );10781079 await act(() => {1080 resolveText('block again');1081 });10821083 expect(getMeaningfulChildren(document)).toEqual(1084 <html>1085 <head>1086 <link rel="stylesheet" href="one1" data-precedence="one" />1087 <link rel="stylesheet" href="one2" data-precedence="one" />1088 <style data-href="one3" data-precedence="one">1089 {css}1090 </style>1091 <style data-href="two1" data-precedence="two">1092 {css}1093 </style>1094 <link rel="stylesheet" href="two2" data-precedence="two" />1095 <style data-href="two3" data-precedence="two">1096 {css}1097 </style>1098 <link rel="stylesheet" href="three1" data-precedence="three" />1099 <style data-href="three2" data-precedence="three">1100 {css}1101 </style>1102 <link rel="stylesheet" href="three3" data-precedence="three" />1103 <style data-href="four1" data-precedence="four">1104 {css}1105 </style>1106 <style data-href="four2" data-precedence="four">1107 {css}1108 </style>1109 <link rel="stylesheet" href="four3" data-precedence="four" />1110 <link rel="stylesheet" href="five1" data-precedence="five" />1111 <style data-href="six1" data-precedence="six">1112 {css}1113 </style>1114 </head>1115 <body>1116 <link rel="preload" href="one2" as="style" />1117 <link rel="preload" href="two2" as="style" />1118 <link rel="preload" href="five1" as="style" />1119 <link rel="preload" href="three3" as="style" />1120 <link rel="preload" href="four3" as="style" />1121 </body>1122 </html>,1123 );11241125 ReactDOMClient.hydrateRoot(1126 document,1127 <html>1128 <body>1129 <link rel="stylesheet" href="one4" precedence="one" />1130 <style href="two4" precedence="two">1131 {css}1132 </style>1133 <link rel="stylesheet" href="three4" precedence="three" />1134 <style href="four4" precedence="four">1135 {css}1136 </style>1137 <link rel="stylesheet" href="seven1" precedence="seven" />1138 <style href="eight1" precedence="eight">1139 {css}1140 </style>1141 </body>1142 </html>,1143 );1144 await waitForAll([]);1145 await act(() => {1146 loadPreloads();1147 loadStylesheets();1148 });1149 await assertLog([1150 'load preload: one4',1151 'load preload: three4',1152 'load preload: seven1',1153 'load preload: one2',1154 'load preload: two2',1155 'load preload: five1',1156 'load preload: three3',1157 'load preload: four3',1158 'load stylesheet: one1',1159 'load stylesheet: one2',1160 'load stylesheet: one4',1161 'load stylesheet: two2',1162 'load stylesheet: three1',1163 'load stylesheet: three3',1164 'load stylesheet: three4',1165 'load stylesheet: four3',1166 'load stylesheet: five1',1167 'load stylesheet: seven1',1168 ]);11691170 expect(getMeaningfulChildren(document)).toEqual(1171 <html>1172 <head>1173 <link rel="stylesheet" href="one1" data-precedence="one" />1174 <link rel="stylesheet" href="one2" data-precedence="one" />1175 <style data-href="one3" data-precedence="one">1176 {css}1177 </style>1178 <link rel="stylesheet" href="one4" data-precedence="one" />1179 <style data-href="two1" data-precedence="two">1180 {css}1181 </style>1182 <link rel="stylesheet" href="two2" data-precedence="two" />1183 <style data-href="two3" data-precedence="two">1184 {css}1185 </style>1186 <style data-href="two4" data-precedence="two">1187 {css}1188 </style>1189 <link rel="stylesheet" href="three1" data-precedence="three" />1190 <style data-href="three2" data-precedence="three">1191 {css}1192 </style>1193 <link rel="stylesheet" href="three3" data-precedence="three" />1194 <link rel="stylesheet" href="three4" data-precedence="three" />1195 <style data-href="four1" data-precedence="four">1196 {css}1197 </style>1198 <style data-href="four2" data-precedence="four">1199 {css}1200 </style>1201 <link rel="stylesheet" href="four3" data-precedence="four" />1202 <style data-href="four4" data-precedence="four">1203 {css}1204 </style>1205 <link rel="stylesheet" href="five1" data-precedence="five" />1206 <style data-href="six1" data-precedence="six">1207 {css}1208 </style>1209 <link rel="stylesheet" href="seven1" data-precedence="seven" />1210 <style data-href="eight1" data-precedence="eight">1211 {css}1212 </style>1213 <link rel="preload" href="one4" as="style" />1214 <link rel="preload" href="three4" as="style" />1215 <link rel="preload" href="seven1" as="style" />1216 </head>1217 <body>1218 <link rel="preload" href="one2" as="style" />1219 <link rel="preload" href="two2" as="style" />1220 <link rel="preload" href="five1" as="style" />1221 <link rel="preload" href="three3" as="style" />1222 <link rel="preload" href="four3" as="style" />1223 </body>1224 </html>,1225 );1226 });12271228 it('client renders a boundary if a style Resource dependency fails to load', async () => {1229 function App() {1230 return (1231 <html>1232 <head />1233 <body>1234 <Suspense fallback="loading...">1235 <BlockedOn value="unblock">1236 <link rel="stylesheet" href="foo" precedence="arbitrary" />1237 <link rel="stylesheet" href="bar" precedence="arbitrary" />1238 Hello1239 </BlockedOn>1240 </Suspense>1241 </body>1242 </html>1243 );1244 }1245 await act(() => {1246 const {pipe} = renderToPipeableStream(<App />);1247 pipe(writable);1248 });12491250 expect(getMeaningfulChildren(document)).toEqual(1251 <html>1252 <head />1253 <body>loading...</body>1254 </html>,1255 );12561257 await act(() => {1258 resolveText('unblock');1259 });12601261 expect(getMeaningfulChildren(document)).toEqual(1262 <html>1263 <head>1264 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1265 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1266 </head>1267 <body>1268 loading...1269 <link rel="preload" href="foo" as="style" />1270 <link rel="preload" href="bar" as="style" />1271 </body>1272 </html>,1273 );12741275 errorStylesheets(['bar']);1276 assertLog(['error stylesheet: bar']);12771278 await waitForAll([]);12791280 const boundaryTemplateInstance = document.getElementById('B:0');1281 const suspenseInstance = boundaryTemplateInstance.previousSibling;12821283 expect(suspenseInstance.data).toEqual('$!');1284 expect(boundaryTemplateInstance.dataset.dgst).toBe('CSS failed to load');12851286 expect(getMeaningfulChildren(document)).toEqual(1287 <html>1288 <head>1289 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1290 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1291 </head>1292 <body>1293 loading...1294 <link rel="preload" href="foo" as="style" />1295 <link rel="preload" href="bar" as="style" />1296 </body>1297 </html>,1298 );12991300 const errors = [];1301 ReactDOMClient.hydrateRoot(document, <App />, {1302 onRecoverableError(err, errInfo) {1303 errors.push(err.message);1304 errors.push(err.digest);1305 },1306 });1307 await waitForAll([]);1308 // When binding a stylesheet that was SSR'd in a boundary reveal there is a loadingState promise1309 // We need to use that promise to resolve the suspended commit because we don't know if the load or error1310 // events have already fired. This requires the load to be awaited for the commit to have a chance to flush1311 // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar1312 // to thenables however this slightly increases the fizz runtime code size.1313 await clientAct(() => loadStylesheets());1314 assertLog(['load stylesheet: foo']);1315 expect(getMeaningfulChildren(document)).toEqual(1316 <html>1317 <head>1318 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1319 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1320 </head>1321 <body>1322 <link rel="preload" href="foo" as="style" />1323 <link rel="preload" href="bar" as="style" />1324 Hello1325 </body>1326 </html>,1327 );1328 expect(errors).toEqual([1329 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1330 'CSS failed to load',1331 ]);1332 });13331334 it('treats stylesheet links with a precedence as a resource', async () => {1335 await act(() => {1336 const {pipe} = renderToPipeableStream(1337 <html>1338 <head />1339 <body>1340 <link rel="stylesheet" href="foo" precedence="arbitrary" />1341 Hello1342 </body>1343 </html>,1344 );1345 pipe(writable);1346 });1347 expect(getMeaningfulChildren(document)).toEqual(1348 <html>1349 <head>1350 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1351 </head>1352 <body>Hello</body>1353 </html>,1354 );13551356 ReactDOMClient.hydrateRoot(1357 document,1358 <html>1359 <head />1360 <body>Hello</body>1361 </html>,1362 );1363 await waitForAll([]);1364 expect(getMeaningfulChildren(document)).toEqual(1365 <html>1366 <head>1367 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1368 </head>1369 <body>Hello</body>1370 </html>,1371 );1372 });13731374 it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => {1375 // If you render many of these as siblings the values get emitted as a single text with no separator sometimes1376 // because the link gets elided as a resource1377 function AsyncTextWithResource({text, href, precedence}) {1378 const value = readText(text);1379 return (1380 <>1381 {value}1382 <link rel="stylesheet" href={href} precedence={precedence} />1383 </>1384 );1385 }13861387 await act(() => {1388 const {pipe} = renderToPipeableStream(1389 <html>1390 <head />1391 <body>1392 <AsyncTextWithResource text="foo" href="foo" precedence="one" />1393 <AsyncTextWithResource text="bar" href="bar" precedence="two" />1394 <AsyncTextWithResource text="baz" href="baz" precedence="three" />1395 </body>1396 </html>,1397 );1398 pipe(writable);1399 resolveText('foo');1400 resolveText('bar');1401 resolveText('baz');1402 });14031404 expect(getMeaningfulChildren(document)).toEqual(1405 <html>1406 <head>1407 <link rel="stylesheet" href="foo" data-precedence="one" />1408 <link rel="stylesheet" href="bar" data-precedence="two" />1409 <link rel="stylesheet" href="baz" data-precedence="three" />1410 </head>1411 <body>1412 {'foo'}1413 {'bar'}1414 {'baz'}1415 </body>1416 </html>,1417 );1418 });14191420 it('hoists late stylesheets the correct precedence', async () => {1421 function PresetPrecedence() {1422 ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});1423 }1424 await act(() => {1425 const {pipe} = renderToPipeableStream(1426 <html>1427 <head />1428 <body>1429 <link rel="stylesheet" href="initial" precedence="one" />1430 <PresetPrecedence />1431 <div>1432 <Suspense fallback="loading foo bar...">1433 <div>foo</div>1434 <link rel="stylesheet" href="foo" precedence="one" />1435 <BlockedOn value="bar">1436 <div>bar</div>1437 <link rel="stylesheet" href="bar" precedence="default" />1438 </BlockedOn>1439 </Suspense>1440 </div>1441 <div>1442 <Suspense fallback="loading bar baz qux...">1443 <BlockedOn value="bar">1444 <div>bar</div>1445 <link rel="stylesheet" href="bar" precedence="default" />1446 </BlockedOn>1447 <BlockedOn value="baz">1448 <div>baz</div>1449 <link rel="stylesheet" href="baz" precedence="two" />1450 </BlockedOn>1451 <BlockedOn value="qux">1452 <div>qux</div>1453 <link rel="stylesheet" href="qux" precedence="one" />1454 </BlockedOn>1455 </Suspense>1456 </div>1457 <div>1458 <Suspense fallback="loading bar baz qux...">1459 <BlockedOn value="unblock">1460 <BlockedOn value="bar">1461 <div>bar</div>1462 <link rel="stylesheet" href="bar" precedence="default" />1463 </BlockedOn>1464 <BlockedOn value="baz">1465 <div>baz</div>1466 <link rel="stylesheet" href="baz" precedence="two" />1467 </BlockedOn>1468 <BlockedOn value="qux">1469 <div>qux</div>1470 <link rel="stylesheet" href="qux" precedence="one" />1471 </BlockedOn>1472 </BlockedOn>1473 </Suspense>1474 </div>1475 </body>1476 </html>,1477 );1478 pipe(writable);1479 });14801481 expect(getMeaningfulChildren(document)).toEqual(1482 <html>1483 <head>1484 <link rel="stylesheet" href="initial" data-precedence="one" />1485 <link rel="stylesheet" href="foo" data-precedence="one" />1486 <link rel="stylesheet" href="preset" data-precedence="preset" />1487 </head>1488 <body>1489 <div>loading foo bar...</div>1490 <div>loading bar baz qux...</div>1491 <div>loading bar baz qux...</div>1492 </body>1493 </html>,1494 );14951496 await act(() => {1497 resolveText('foo');1498 resolveText('bar');1499 });15001501 expect(getMeaningfulChildren(document)).toEqual(1502 <html>1503 <head>1504 <link rel="stylesheet" href="initial" data-precedence="one" />1505 <link rel="stylesheet" href="foo" data-precedence="one" />1506 <link rel="stylesheet" href="preset" data-precedence="preset" />1507 <link rel="stylesheet" href="bar" data-precedence="default" />1508 </head>1509 <body>1510 <div>loading foo bar...</div>1511 <div>loading bar baz qux...</div>1512 <div>loading bar baz qux...</div>1513 <link rel="preload" href="bar" as="style" />1514 </body>1515 </html>,1516 );15171518 await act(() => {1519 const link = document.querySelector('link[rel="stylesheet"][href="foo"]');1520 const event = document.createEvent('Events');1521 event.initEvent('load', true, true);1522 link.dispatchEvent(event);1523 });15241525 expect(getMeaningfulChildren(document)).toEqual(1526 <html>1527 <head>1528 <link rel="stylesheet" href="initial" data-precedence="one" />1529 <link rel="stylesheet" href="foo" data-precedence="one" />1530 <link rel="stylesheet" href="preset" data-precedence="preset" />1531 <link rel="stylesheet" href="bar" data-precedence="default" />1532 </head>1533 <body>1534 <div>loading foo bar...</div>1535 <div>loading bar baz qux...</div>1536 <div>loading bar baz qux...</div>1537 <link rel="preload" href="bar" as="style" />1538 </body>1539 </html>,1540 );15411542 await act(() => {1543 const link = document.querySelector('link[rel="stylesheet"][href="bar"]');1544 const event = document.createEvent('Events');1545 event.initEvent('load', true, true);1546 link.dispatchEvent(event);1547 });15481549 expect(getMeaningfulChildren(document)).toEqual(1550 <html>1551 <head>1552 <link rel="stylesheet" href="initial" data-precedence="one" />1553 <link rel="stylesheet" href="foo" data-precedence="one" />1554 <link rel="stylesheet" href="preset" data-precedence="preset" />1555 <link rel="stylesheet" href="bar" data-precedence="default" />1556 </head>1557 <body>1558 <div>1559 <div>foo</div>1560 <div>bar</div>1561 </div>1562 <div>loading bar baz qux...</div>1563 <div>loading bar baz qux...</div>1564 <link rel="preload" href="bar" as="style" />1565 </body>1566 </html>,1567 );15681569 await act(() => {1570 resolveText('baz');1571 });15721573 expect(getMeaningfulChildren(document)).toEqual(1574 <html>1575 <head>1576 <link rel="stylesheet" href="initial" data-precedence="one" />1577 <link rel="stylesheet" href="foo" data-precedence="one" />1578 <link rel="stylesheet" href="preset" data-precedence="preset" />1579 <link rel="stylesheet" href="bar" data-precedence="default" />1580 </head>1581 <body>1582 <div>1583 <div>foo</div>1584 <div>bar</div>1585 </div>1586 <div>loading bar baz qux...</div>1587 <div>loading bar baz qux...</div>1588 <link rel="preload" as="style" href="bar" />1589 <link rel="preload" as="style" href="baz" />1590 </body>1591 </html>,1592 );15931594 await act(() => {1595 resolveText('qux');1596 });15971598 expect(getMeaningfulChildren(document)).toEqual(1599 <html>1600 <head>1601 <link rel="stylesheet" href="initial" data-precedence="one" />1602 <link rel="stylesheet" href="foo" data-precedence="one" />1603 <link rel="stylesheet" href="qux" data-precedence="one" />1604 <link rel="stylesheet" href="preset" data-precedence="preset" />1605 <link rel="stylesheet" href="bar" data-precedence="default" />1606 <link rel="stylesheet" href="baz" data-precedence="two" />1607 </head>1608 <body>1609 <div>1610 <div>foo</div>1611 <div>bar</div>1612 </div>1613 <div>loading bar baz qux...</div>1614 <div>loading bar baz qux...</div>1615 <link rel="preload" as="style" href="bar" />1616 <link rel="preload" as="style" href="baz" />1617 <link rel="preload" as="style" href="qux" />1618 </body>1619 </html>,1620 );16211622 await act(() => {1623 const bazlink = document.querySelector(1624 'link[rel="stylesheet"][href="baz"]',1625 );1626 const quxlink = document.querySelector(1627 'link[rel="stylesheet"][href="qux"]',1628 );1629 const presetLink = document.querySelector(1630 'link[rel="stylesheet"][href="preset"]',1631 );1632 const event = document.createEvent('Events');1633 event.initEvent('load', true, true);1634 bazlink.dispatchEvent(event);1635 quxlink.dispatchEvent(event);1636 presetLink.dispatchEvent(event);1637 });16381639 expect(getMeaningfulChildren(document)).toEqual(1640 <html>1641 <head>1642 <link rel="stylesheet" href="initial" data-precedence="one" />1643 <link rel="stylesheet" href="foo" data-precedence="one" />1644 <link rel="stylesheet" href="qux" data-precedence="one" />1645 <link rel="stylesheet" href="preset" data-precedence="preset" />1646 <link rel="stylesheet" href="bar" data-precedence="default" />1647 <link rel="stylesheet" href="baz" data-precedence="two" />1648 </head>1649 <body>1650 <div>1651 <div>foo</div>1652 <div>bar</div>1653 </div>1654 <div>1655 <div>bar</div>1656 <div>baz</div>1657 <div>qux</div>1658 </div>1659 <div>loading bar baz qux...</div>1660 <link rel="preload" as="style" href="bar" />1661 <link rel="preload" as="style" href="baz" />1662 <link rel="preload" as="style" href="qux" />1663 </body>1664 </html>,1665 );16661667 await act(() => {1668 resolveText('unblock');1669 });16701671 expect(getMeaningfulChildren(document)).toEqual(1672 <html>1673 <head>1674 <link rel="stylesheet" href="initial" data-precedence="one" />1675 <link rel="stylesheet" href="foo" data-precedence="one" />1676 <link rel="stylesheet" href="qux" data-precedence="one" />1677 <link rel="stylesheet" href="preset" data-precedence="preset" />1678 <link rel="stylesheet" href="bar" data-precedence="default" />1679 <link rel="stylesheet" href="baz" data-precedence="two" />1680 </head>1681 <body>1682 <div>1683 <div>foo</div>1684 <div>bar</div>1685 </div>1686 <div>1687 <div>bar</div>1688 <div>baz</div>1689 <div>qux</div>1690 </div>1691 <div>1692 <div>bar</div>1693 <div>baz</div>1694 <div>qux</div>1695 </div>1696 <link rel="preload" as="style" href="bar" />1697 <link rel="preload" as="style" href="baz" />1698 <link rel="preload" as="style" href="qux" />1699 </body>1700 </html>,1701 );1702 });17031704 it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => {1705 await act(() => {1706 const {pipe} = renderToPipeableStream(1707 <html>1708 <head />1709 <body>1710 <div>1711 outer1712 <link rel="stylesheet" href="1one" precedence="one" />1713 <link rel="stylesheet" href="1two" precedence="two" />1714 <link rel="stylesheet" href="1three" precedence="three" />1715 <link rel="stylesheet" href="1four" precedence="four" />1716 <Suspense fallback={null}>1717 <div>1718 middle1719 <link rel="stylesheet" href="2one" precedence="one" />1720 <link rel="stylesheet" href="2two" precedence="two" />1721 <link rel="stylesheet" href="2three" precedence="three" />1722 <link rel="stylesheet" href="2four" precedence="four" />1723 <Suspense fallback={null}>1724 <div>1725 inner1726 <link rel="stylesheet" href="3five" precedence="five" />1727 <link rel="stylesheet" href="3one" precedence="one" />1728 <link rel="stylesheet" href="3two" precedence="two" />1729 <link rel="stylesheet" href="3three" precedence="three" />1730 <link rel="stylesheet" href="3four" precedence="four" />1731 </div>1732 </Suspense>1733 </div>1734 </Suspense>1735 <Suspense fallback={null}>1736 <div>middle</div>1737 <link rel="stylesheet" href="4one" precedence="one" />1738 <link rel="stylesheet" href="4two" precedence="two" />1739 <link rel="stylesheet" href="4three" precedence="three" />1740 <link rel="stylesheet" href="4four" precedence="four" />1741 </Suspense>1742 </div>1743 </body>1744 </html>,1745 );1746 pipe(writable);1747 });17481749 expect(getMeaningfulChildren(document)).toEqual(1750 <html>1751 <head>1752 <link rel="stylesheet" href="1one" data-precedence="one" />1753 <link rel="stylesheet" href="2one" data-precedence="one" />1754 <link rel="stylesheet" href="3one" data-precedence="one" />1755 <link rel="stylesheet" href="4one" data-precedence="one" />17561757 <link rel="stylesheet" href="1two" data-precedence="two" />1758 <link rel="stylesheet" href="2two" data-precedence="two" />1759 <link rel="stylesheet" href="3two" data-precedence="two" />1760 <link rel="stylesheet" href="4two" data-precedence="two" />17611762 <link rel="stylesheet" href="1three" data-precedence="three" />1763 <link rel="stylesheet" href="2three" data-precedence="three" />1764 <link rel="stylesheet" href="3three" data-precedence="three" />1765 <link rel="stylesheet" href="4three" data-precedence="three" />17661767 <link rel="stylesheet" href="1four" data-precedence="four" />1768 <link rel="stylesheet" href="2four" data-precedence="four" />1769 <link rel="stylesheet" href="3four" data-precedence="four" />1770 <link rel="stylesheet" href="4four" data-precedence="four" />17711772 <link rel="stylesheet" href="3five" data-precedence="five" />1773 </head>1774 <body>1775 <div>1776 outer1777 <div>1778 middle<div>inner</div>1779 </div>1780 <div>middle</div>1781 </div>1782 </body>1783 </html>,1784 );1785 });17861787 it('stylesheet resources are inserted according to precedence order on the client', async () => {1788 await act(() => {1789 const {pipe} = renderToPipeableStream(1790 <html>1791 <head />1792 <body>1793 <div>1794 <link rel="stylesheet" href="foo" precedence="one" />1795 <link rel="stylesheet" href="bar" precedence="two" />1796 Hello1797 </div>1798 </body>1799 </html>,1800 );1801 pipe(writable);1802 });18031804 expect(getMeaningfulChildren(document)).toEqual(1805 <html>1806 <head>1807 <link rel="stylesheet" href="foo" data-precedence="one" />1808 <link rel="stylesheet" href="bar" data-precedence="two" />1809 </head>1810 <body>1811 <div>Hello</div>1812 </body>1813 </html>,1814 );18151816 const root = ReactDOMClient.hydrateRoot(1817 document,1818 <html>1819 <head />1820 <body>1821 <div>1822 <link rel="stylesheet" href="foo" precedence="one" />1823 <link rel="stylesheet" href="bar" precedence="two" />1824 Hello1825 </div>1826 </body>1827 </html>,1828 );1829 await waitForAll([]);1830 expect(getMeaningfulChildren(document)).toEqual(1831 <html>1832 <head>1833 <link rel="stylesheet" href="foo" data-precedence="one" />1834 <link rel="stylesheet" href="bar" data-precedence="two" />1835 </head>1836 <body>1837 <div>Hello</div>1838 </body>1839 </html>,1840 );18411842 root.render(1843 <html>1844 <head />1845 <body>1846 <div>Goodbye</div>1847 <link rel="stylesheet" href="baz" precedence="one" />1848 </body>1849 </html>,1850 );1851 await waitForAll([]);1852 await act(() => {1853 loadPreloads();1854 loadStylesheets();1855 });1856 await assertLog([1857 'load preload: baz',1858 'load stylesheet: foo',1859 'load stylesheet: baz',1860 'load stylesheet: bar',1861 ]);1862 expect(getMeaningfulChildren(document)).toEqual(1863 <html>1864 <head>1865 <link rel="stylesheet" href="foo" data-precedence="one" />1866 <link rel="stylesheet" href="baz" data-precedence="one" />1867 <link rel="stylesheet" href="bar" data-precedence="two" />1868 <link rel="preload" as="style" href="baz" />1869 </head>1870 <body>1871 <div>Goodbye</div>1872 </body>1873 </html>,1874 );1875 });18761877 it('inserts preloads in render phase eagerly', async () => {1878 function Throw() {1879 throw new Error('Uh oh!');1880 }1881 class ErrorBoundary extends React.Component {1882 state = {hasError: false, error: null};1883 static getDerivedStateFromError(error) {1884 return {1885 hasError: true,1886 error,1887 };1888 }1889 render() {1890 if (this.state.hasError) {1891 return this.state.error.message;1892 }1893 return this.props.children;1894 }1895 }18961897 const root = ReactDOMClient.createRoot(container);1898 root.render(1899 <ErrorBoundary>1900 <link rel="stylesheet" href="foo" precedence="default" />1901 <div>foo</div>1902 <Throw />1903 </ErrorBoundary>,1904 );1905 await waitForAll([]);1906 expect(getMeaningfulChildren(document)).toEqual(1907 <html>1908 <head>1909 <link rel="preload" href="foo" as="style" />1910 </head>1911 <body>1912 <div id="container">Uh oh!</div>1913 </body>1914 </html>,1915 );1916 });19171918 it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => {1919 await act(() => {1920 const {pipe} = renderToPipeableStream(1921 <html>1922 <head />1923 <body>1924 <div>1925 <Suspense fallback="loading foo...">1926 <BlockedOn value="foo">1927 <div>foo</div>1928 <link rel="stylesheet" href="foo" precedence="default" />1929 <Suspense fallback="loading bar...">1930 <BlockedOn value="bar">1931 <div>bar</div>1932 <link rel="stylesheet" href="bar" precedence="default" />1933 <Suspense fallback="loading baz...">1934 <BlockedOn value="baz">1935 <div>baz</div>1936 <link1937 rel="stylesheet"1938 href="baz"1939 precedence="default"1940 />1941 </BlockedOn>1942 </Suspense>1943 </BlockedOn>1944 </Suspense>1945 </BlockedOn>1946 </Suspense>1947 </div>1948 </body>1949 </html>,1950 );1951 pipe(writable);1952 });19531954 expect(getMeaningfulChildren(document)).toEqual(1955 <html>1956 <head />1957 <body>1958 <div>loading foo...</div>1959 </body>1960 </html>,1961 );19621963 await act(() => {1964 resolveText('bar');1965 });1966 expect(getMeaningfulChildren(document)).toEqual(1967 <html>1968 <head />1969 <body>1970 <div>loading foo...</div>1971 </body>1972 </html>,1973 );19741975 await act(() => {1976 resolveText('baz');1977 });1978 expect(getMeaningfulChildren(document)).toEqual(1979 <html>1980 <head />1981 <body>1982 <div>loading foo...</div>1983 </body>1984 </html>,1985 );19861987 await act(() => {1988 resolveText('foo');1989 });1990 expect(getMeaningfulChildren(document)).toEqual(1991 <html>1992 <head>1993 <link rel="stylesheet" href="foo" data-precedence="default" />1994 <link rel="stylesheet" href="bar" data-precedence="default" />1995 <link rel="stylesheet" href="baz" data-precedence="default" />1996 </head>1997 <body>1998 <div>loading foo...</div>1999 <link rel="preload" href="foo" as="style" />2000 <link rel="preload" href="bar" as="style" />
Findings
✓ No findings reported for this file.