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 ...(gate('enableTrustedTypesIntegration')606 ? [607 'Encountered a script tag while rendering React component. ' +608 'Scripts inside React components are never executed when rendering on the client. ' +609 'Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +610 ' in script (at **)',611 ]612 : []),613 ]);614615 root.render(616 <html>617 <script async={true} onLoad={() => {}} href="bar" />618 <body>foo</body>619 </html>,620 );621 await waitForAll([]);622 assertConsoleErrorDev([623 'Cannot render a <script> with onLoad or onError listeners outside the main document. ' +624 'Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or ' +625 'somewhere in the <body>.\n' +626 ' in html (at **)',627 ]);628629 root.render(630 <>631 <link rel="foo" onLoad={() => {}} href="bar" />632 <html>633 <body>foo</body>634 </html>635 </>,636 );637 await waitForAll([]);638 assertConsoleErrorDev([639 'Cannot render a <link> with onLoad or onError listeners outside the main document. ' +640 'Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or ' +641 'somewhere in the <body>.',642 ]);643 return;644 });645646 it('can acquire a resource after releasing it in the same commit', async () => {647 const root = ReactDOMClient.createRoot(container);648 root.render(649 <>650 <script async={true} src="foo" />651 </>,652 );653 await waitForAll([]);654 expect(getMeaningfulChildren(document)).toEqual(655 <html>656 <head>657 <script async="" src="foo" />658 </head>659 <body>660 <div id="container" />661 </body>662 </html>,663 );664665 root.render(666 <>667 {null}668 <script data-new="new" async={true} src="foo" />669 </>,670 );671 await waitForAll([]);672 // we don't see the attribute because the resource is the same and was not reconstructed673 expect(getMeaningfulChildren(document)).toEqual(674 <html>675 <head>676 <script async="" src="foo" />677 </head>678 <body>679 <div id="container" />680 </body>681 </html>,682 );683 });684685 it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {686 const chunks = [];687688 writable.on('data', chunk => {689 chunks.push(chunk);690 });691692 await act(() => {693 const {pipe} = renderToPipeableStream(694 <>695 <title>foo</title>696 <html>697 <body>bar</body>698 </html>699 <script async={true} src="foo" />700 </>,701 );702 pipe(writable);703 });704 expect(chunks).toEqual([705 '<!DOCTYPE html><html><head><script async="" src="foo"></script>' +706 (gate(flags => flags.shouldUseFizzExternalRuntime)707 ? '<script src="react-dom/unstable_server-external-runtime" async=""></script>'708 : '') +709 (gate(flags => flags.enableFizzBlockingRender)710 ? '<link rel="expect" href="#_R_" blocking="render"/>'711 : '') +712 '<title>foo</title></head>' +713 '<body>bar' +714 (gate(flags => flags.enableFizzBlockingRender)715 ? '<template id="_R_"></template>'716 : ''),717 '</body></html>',718 ]);719 });720721 it('dedupes if the external runtime is explicitly loaded using preinit', async () => {722 const unstable_externalRuntimeSrc = 'src-of-external-runtime';723 function App() {724 ReactDOM.preinit(unstable_externalRuntimeSrc, {as: 'script'});725 return (726 <div>727 <Suspense fallback={<h1>Loading...</h1>}>728 <AsyncText text="Hello" />729 </Suspense>730 </div>731 );732 }733734 await act(() => {735 const {pipe} = renderToPipeableStream(736 <html>737 <head />738 <body>739 <App />740 </body>741 </html>,742 {743 unstable_externalRuntimeSrc,744 },745 );746 pipe(writable);747 });748749 expect(750 Array.from(document.querySelectorAll('script[async]')).map(751 n => n.outerHTML,752 ),753 ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);754 });755756 it('can send style insertion implementation independent of boundary commpletion instruction implementation', async () => {757 await act(() => {758 renderToPipeableStream(759 <html>760 <body>761 <Suspense fallback="loading foo...">762 <BlockedOn value="foo">foo</BlockedOn>763 </Suspense>764 <Suspense fallback="loading bar...">765 <BlockedOn value="bar">766 <link rel="stylesheet" href="bar" precedence="bar" />767 bar768 </BlockedOn>769 </Suspense>770 </body>771 </html>,772 ).pipe(writable);773 });774775 expect(getMeaningfulChildren(document)).toEqual(776 <html>777 <head />778 <body>779 {'loading foo...'}780 {'loading bar...'}781 </body>782 </html>,783 );784785 await act(() => {786 resolveText('foo');787 });788 expect(getMeaningfulChildren(document)).toEqual(789 <html>790 <head />791 <body>792 foo793 {'loading bar...'}794 </body>795 </html>,796 );797 await act(() => {798 resolveText('bar');799 });800 expect(getMeaningfulChildren(document)).toEqual(801 <html>802 <head>803 <link rel="stylesheet" href="bar" data-precedence="bar" />804 </head>805 <body>806 foo807 {'loading bar...'}808 <link rel="preload" href="bar" as="style" />809 </body>810 </html>,811 );812 });813814 it('can avoid inserting a late stylesheet if it already rendered on the client', async () => {815 await act(() => {816 renderToPipeableStream(817 <html>818 <body>819 <Suspense fallback="loading foo...">820 <BlockedOn value="foo">821 <link rel="stylesheet" href="foo" precedence="foo" />822 foo823 </BlockedOn>824 </Suspense>825 <Suspense fallback="loading bar...">826 <BlockedOn value="bar">827 <link rel="stylesheet" href="bar" precedence="bar" />828 bar829 </BlockedOn>830 </Suspense>831 </body>832 </html>,833 ).pipe(writable);834 });835836 expect(getMeaningfulChildren(document)).toEqual(837 <html>838 <head />839 <body>840 {'loading foo...'}841 {'loading bar...'}842 </body>843 </html>,844 );845846 ReactDOMClient.hydrateRoot(847 document,848 <html>849 <body>850 <link rel="stylesheet" href="foo" precedence="foo" />851 <Suspense fallback="loading foo...">852 <link rel="stylesheet" href="foo" precedence="foo" />853 foo854 </Suspense>855 <Suspense fallback="loading bar...">856 <link rel="stylesheet" href="bar" precedence="bar" />857 bar858 </Suspense>859 </body>860 </html>,861 );862 await waitForAll([]);863 loadPreloads();864 await assertLog(['load preload: foo']);865 expect(getMeaningfulChildren(document)).toEqual(866 <html>867 <head>868 <link rel="stylesheet" href="foo" data-precedence="foo" />869 <link as="style" href="foo" rel="preload" />870 </head>871 <body>872 {'loading foo...'}873 {'loading bar...'}874 </body>875 </html>,876 );877878 await act(() => {879 resolveText('bar');880 });881 await act(() => {882 loadStylesheets();883 });884 await assertLog(['load stylesheet: foo', 'load stylesheet: bar']);885 expect(getMeaningfulChildren(document)).toEqual(886 <html>887 <head>888 <link rel="stylesheet" href="foo" data-precedence="foo" />889 <link rel="stylesheet" href="bar" data-precedence="bar" />890 <link as="style" href="foo" rel="preload" />891 </head>892 <body>893 {'loading foo...'}894 {'bar'}895 <link as="style" href="bar" rel="preload" />896 </body>897 </html>,898 );899900 await act(() => {901 resolveText('foo');902 });903 await act(() => {904 loadStylesheets();905 });906 await assertLog([]);907 expect(getMeaningfulChildren(document)).toEqual(908 <html>909 <head>910 <link rel="stylesheet" href="foo" data-precedence="foo" />911 <link rel="stylesheet" href="bar" data-precedence="bar" />912 <link as="style" href="foo" rel="preload" />913 </head>914 <body>915 {'foo'}916 {'bar'}917 <link as="style" href="bar" rel="preload" />918 <link as="style" href="foo" rel="preload" />919 </body>920 </html>,921 );922 });923924 it('can hoist <link rel="stylesheet" .../> and <style /> tags together, respecting order of discovery', async () => {925 const css = `926body {927 background-color: red;928}`;929930 await act(() => {931 renderToPipeableStream(932 <html>933 <body>934 <link rel="stylesheet" href="one1" precedence="one" />935 <style href="two1" precedence="two">936 {css}937 </style>938 <link rel="stylesheet" href="three1" precedence="three" />939 <style href="four1" precedence="four">940 {css}941 </style>942 <Suspense>943 <BlockedOn value="block">944 <link rel="stylesheet" href="one2" precedence="one" />945 <link rel="stylesheet" href="two2" precedence="two" />946 <style href="three2" precedence="three">947 {css}948 </style>949 <style href="four2" precedence="four">950 {css}951 </style>952 <link rel="stylesheet" href="five1" precedence="five" />953 </BlockedOn>954 </Suspense>955 <Suspense>956 <BlockedOn value="block2">957 <style href="one3" precedence="one">958 {css}959 </style>960 <style href="two3" precedence="two">961 {css}962 </style>963 <link rel="stylesheet" href="three3" precedence="three" />964 <link rel="stylesheet" href="four3" precedence="four" />965 <style href="six1" precedence="six">966 {css}967 </style>968 </BlockedOn>969 </Suspense>970 <Suspense>971 <BlockedOn value="block again">972 <link rel="stylesheet" href="one2" precedence="one" />973 <link rel="stylesheet" href="two2" precedence="two" />974 <style href="three2" precedence="three">975 {css}976 </style>977 <style href="four2" precedence="four">978 {css}979 </style>980 <link rel="stylesheet" href="five1" precedence="five" />981 </BlockedOn>982 </Suspense>983 </body>984 </html>,985 ).pipe(writable);986 });987988 expect(getMeaningfulChildren(document)).toEqual(989 <html>990 <head>991 <link rel="stylesheet" href="one1" data-precedence="one" />992 <style data-href="two1" data-precedence="two">993 {css}994 </style>995 <link rel="stylesheet" href="three1" data-precedence="three" />996 <style data-href="four1" data-precedence="four">997 {css}998 </style>999 </head>1000 <body />1001 </html>,1002 );10031004 await act(() => {1005 resolveText('block');1006 });10071008 expect(getMeaningfulChildren(document)).toEqual(1009 <html>1010 <head>1011 <link rel="stylesheet" href="one1" data-precedence="one" />1012 <link rel="stylesheet" href="one2" data-precedence="one" />1013 <style data-href="two1" data-precedence="two">1014 {css}1015 </style>1016 <link rel="stylesheet" href="two2" data-precedence="two" />1017 <link rel="stylesheet" href="three1" data-precedence="three" />1018 <style data-href="three2" data-precedence="three">1019 {css}1020 </style>1021 <style data-href="four1" data-precedence="four">1022 {css}1023 </style>1024 <style data-href="four2" data-precedence="four">1025 {css}1026 </style>1027 <link rel="stylesheet" href="five1" data-precedence="five" />1028 </head>1029 <body>1030 <link rel="preload" href="one2" as="style" />1031 <link rel="preload" href="two2" as="style" />1032 <link rel="preload" href="five1" as="style" />1033 </body>1034 </html>,1035 );10361037 await act(() => {1038 resolveText('block2');1039 });10401041 expect(getMeaningfulChildren(document)).toEqual(1042 <html>1043 <head>1044 <link rel="stylesheet" href="one1" data-precedence="one" />1045 <link rel="stylesheet" href="one2" data-precedence="one" />1046 <style data-href="one3" data-precedence="one">1047 {css}1048 </style>1049 <style data-href="two1" data-precedence="two">1050 {css}1051 </style>1052 <link rel="stylesheet" href="two2" data-precedence="two" />1053 <style data-href="two3" data-precedence="two">1054 {css}1055 </style>1056 <link rel="stylesheet" href="three1" data-precedence="three" />1057 <style data-href="three2" data-precedence="three">1058 {css}1059 </style>1060 <link rel="stylesheet" href="three3" data-precedence="three" />1061 <style data-href="four1" data-precedence="four">1062 {css}1063 </style>1064 <style data-href="four2" data-precedence="four">1065 {css}1066 </style>1067 <link rel="stylesheet" href="four3" data-precedence="four" />1068 <link rel="stylesheet" href="five1" data-precedence="five" />1069 <style data-href="six1" data-precedence="six">1070 {css}1071 </style>1072 </head>1073 <body>1074 <link rel="preload" href="one2" as="style" />1075 <link rel="preload" href="two2" as="style" />1076 <link rel="preload" href="five1" as="style" />1077 <link rel="preload" href="three3" as="style" />1078 <link rel="preload" href="four3" as="style" />1079 </body>1080 </html>,1081 );10821083 await act(() => {1084 resolveText('block again');1085 });10861087 expect(getMeaningfulChildren(document)).toEqual(1088 <html>1089 <head>1090 <link rel="stylesheet" href="one1" data-precedence="one" />1091 <link rel="stylesheet" href="one2" data-precedence="one" />1092 <style data-href="one3" data-precedence="one">1093 {css}1094 </style>1095 <style data-href="two1" data-precedence="two">1096 {css}1097 </style>1098 <link rel="stylesheet" href="two2" data-precedence="two" />1099 <style data-href="two3" data-precedence="two">1100 {css}1101 </style>1102 <link rel="stylesheet" href="three1" data-precedence="three" />1103 <style data-href="three2" data-precedence="three">1104 {css}1105 </style>1106 <link rel="stylesheet" href="three3" data-precedence="three" />1107 <style data-href="four1" data-precedence="four">1108 {css}1109 </style>1110 <style data-href="four2" data-precedence="four">1111 {css}1112 </style>1113 <link rel="stylesheet" href="four3" data-precedence="four" />1114 <link rel="stylesheet" href="five1" data-precedence="five" />1115 <style data-href="six1" data-precedence="six">1116 {css}1117 </style>1118 </head>1119 <body>1120 <link rel="preload" href="one2" as="style" />1121 <link rel="preload" href="two2" as="style" />1122 <link rel="preload" href="five1" as="style" />1123 <link rel="preload" href="three3" as="style" />1124 <link rel="preload" href="four3" as="style" />1125 </body>1126 </html>,1127 );11281129 ReactDOMClient.hydrateRoot(1130 document,1131 <html>1132 <body>1133 <link rel="stylesheet" href="one4" precedence="one" />1134 <style href="two4" precedence="two">1135 {css}1136 </style>1137 <link rel="stylesheet" href="three4" precedence="three" />1138 <style href="four4" precedence="four">1139 {css}1140 </style>1141 <link rel="stylesheet" href="seven1" precedence="seven" />1142 <style href="eight1" precedence="eight">1143 {css}1144 </style>1145 </body>1146 </html>,1147 );1148 await waitForAll([]);1149 await act(() => {1150 loadPreloads();1151 loadStylesheets();1152 });1153 await assertLog([1154 'load preload: one4',1155 'load preload: three4',1156 'load preload: seven1',1157 'load preload: one2',1158 'load preload: two2',1159 'load preload: five1',1160 'load preload: three3',1161 'load preload: four3',1162 'load stylesheet: one1',1163 'load stylesheet: one2',1164 'load stylesheet: one4',1165 'load stylesheet: two2',1166 'load stylesheet: three1',1167 'load stylesheet: three3',1168 'load stylesheet: three4',1169 'load stylesheet: four3',1170 'load stylesheet: five1',1171 'load stylesheet: seven1',1172 ]);11731174 expect(getMeaningfulChildren(document)).toEqual(1175 <html>1176 <head>1177 <link rel="stylesheet" href="one1" data-precedence="one" />1178 <link rel="stylesheet" href="one2" data-precedence="one" />1179 <style data-href="one3" data-precedence="one">1180 {css}1181 </style>1182 <link rel="stylesheet" href="one4" data-precedence="one" />1183 <style data-href="two1" data-precedence="two">1184 {css}1185 </style>1186 <link rel="stylesheet" href="two2" data-precedence="two" />1187 <style data-href="two3" data-precedence="two">1188 {css}1189 </style>1190 <style data-href="two4" data-precedence="two">1191 {css}1192 </style>1193 <link rel="stylesheet" href="three1" data-precedence="three" />1194 <style data-href="three2" data-precedence="three">1195 {css}1196 </style>1197 <link rel="stylesheet" href="three3" data-precedence="three" />1198 <link rel="stylesheet" href="three4" data-precedence="three" />1199 <style data-href="four1" data-precedence="four">1200 {css}1201 </style>1202 <style data-href="four2" data-precedence="four">1203 {css}1204 </style>1205 <link rel="stylesheet" href="four3" data-precedence="four" />1206 <style data-href="four4" data-precedence="four">1207 {css}1208 </style>1209 <link rel="stylesheet" href="five1" data-precedence="five" />1210 <style data-href="six1" data-precedence="six">1211 {css}1212 </style>1213 <link rel="stylesheet" href="seven1" data-precedence="seven" />1214 <style data-href="eight1" data-precedence="eight">1215 {css}1216 </style>1217 <link rel="preload" href="one4" as="style" />1218 <link rel="preload" href="three4" as="style" />1219 <link rel="preload" href="seven1" as="style" />1220 </head>1221 <body>1222 <link rel="preload" href="one2" as="style" />1223 <link rel="preload" href="two2" as="style" />1224 <link rel="preload" href="five1" as="style" />1225 <link rel="preload" href="three3" as="style" />1226 <link rel="preload" href="four3" as="style" />1227 </body>1228 </html>,1229 );1230 });12311232 it('client renders a boundary if a style Resource dependency fails to load', async () => {1233 function App() {1234 return (1235 <html>1236 <head />1237 <body>1238 <Suspense fallback="loading...">1239 <BlockedOn value="unblock">1240 <link rel="stylesheet" href="foo" precedence="arbitrary" />1241 <link rel="stylesheet" href="bar" precedence="arbitrary" />1242 Hello1243 </BlockedOn>1244 </Suspense>1245 </body>1246 </html>1247 );1248 }1249 await act(() => {1250 const {pipe} = renderToPipeableStream(<App />);1251 pipe(writable);1252 });12531254 expect(getMeaningfulChildren(document)).toEqual(1255 <html>1256 <head />1257 <body>loading...</body>1258 </html>,1259 );12601261 await act(() => {1262 resolveText('unblock');1263 });12641265 expect(getMeaningfulChildren(document)).toEqual(1266 <html>1267 <head>1268 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1269 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1270 </head>1271 <body>1272 loading...1273 <link rel="preload" href="foo" as="style" />1274 <link rel="preload" href="bar" as="style" />1275 </body>1276 </html>,1277 );12781279 errorStylesheets(['bar']);1280 assertLog(['error stylesheet: bar']);12811282 await waitForAll([]);12831284 const boundaryTemplateInstance = document.getElementById('B:0');1285 const suspenseInstance = boundaryTemplateInstance.previousSibling;12861287 expect(suspenseInstance.data).toEqual('$!');1288 expect(boundaryTemplateInstance.dataset.dgst).toBe('CSS failed to load');12891290 expect(getMeaningfulChildren(document)).toEqual(1291 <html>1292 <head>1293 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1294 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1295 </head>1296 <body>1297 loading...1298 <link rel="preload" href="foo" as="style" />1299 <link rel="preload" href="bar" as="style" />1300 </body>1301 </html>,1302 );13031304 const errors = [];1305 ReactDOMClient.hydrateRoot(document, <App />, {1306 onRecoverableError(err, errInfo) {1307 errors.push(err.message);1308 errors.push(err.digest);1309 },1310 });1311 await waitForAll([]);1312 // When binding a stylesheet that was SSR'd in a boundary reveal there is a loadingState promise1313 // We need to use that promise to resolve the suspended commit because we don't know if the load or error1314 // events have already fired. This requires the load to be awaited for the commit to have a chance to flush1315 // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar1316 // to thenables however this slightly increases the fizz runtime code size.1317 await clientAct(() => loadStylesheets());1318 assertLog(['load stylesheet: foo']);1319 expect(getMeaningfulChildren(document)).toEqual(1320 <html>1321 <head>1322 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1323 <link rel="stylesheet" href="bar" data-precedence="arbitrary" />1324 </head>1325 <body>1326 <link rel="preload" href="foo" as="style" />1327 <link rel="preload" href="bar" as="style" />1328 Hello1329 </body>1330 </html>,1331 );1332 expect(errors).toEqual([1333 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',1334 'CSS failed to load',1335 ]);1336 });13371338 it('treats stylesheet links with a precedence as a resource', async () => {1339 await act(() => {1340 const {pipe} = renderToPipeableStream(1341 <html>1342 <head />1343 <body>1344 <link rel="stylesheet" href="foo" precedence="arbitrary" />1345 Hello1346 </body>1347 </html>,1348 );1349 pipe(writable);1350 });1351 expect(getMeaningfulChildren(document)).toEqual(1352 <html>1353 <head>1354 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1355 </head>1356 <body>Hello</body>1357 </html>,1358 );13591360 ReactDOMClient.hydrateRoot(1361 document,1362 <html>1363 <head />1364 <body>Hello</body>1365 </html>,1366 );1367 await waitForAll([]);1368 expect(getMeaningfulChildren(document)).toEqual(1369 <html>1370 <head>1371 <link rel="stylesheet" href="foo" data-precedence="arbitrary" />1372 </head>1373 <body>Hello</body>1374 </html>,1375 );1376 });13771378 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 () => {1379 // If you render many of these as siblings the values get emitted as a single text with no separator sometimes1380 // because the link gets elided as a resource1381 function AsyncTextWithResource({text, href, precedence}) {1382 const value = readText(text);1383 return (1384 <>1385 {value}1386 <link rel="stylesheet" href={href} precedence={precedence} />1387 </>1388 );1389 }13901391 await act(() => {1392 const {pipe} = renderToPipeableStream(1393 <html>1394 <head />1395 <body>1396 <AsyncTextWithResource text="foo" href="foo" precedence="one" />1397 <AsyncTextWithResource text="bar" href="bar" precedence="two" />1398 <AsyncTextWithResource text="baz" href="baz" precedence="three" />1399 </body>1400 </html>,1401 );1402 pipe(writable);1403 resolveText('foo');1404 resolveText('bar');1405 resolveText('baz');1406 });14071408 expect(getMeaningfulChildren(document)).toEqual(1409 <html>1410 <head>1411 <link rel="stylesheet" href="foo" data-precedence="one" />1412 <link rel="stylesheet" href="bar" data-precedence="two" />1413 <link rel="stylesheet" href="baz" data-precedence="three" />1414 </head>1415 <body>1416 {'foo'}1417 {'bar'}1418 {'baz'}1419 </body>1420 </html>,1421 );1422 });14231424 it('hoists late stylesheets the correct precedence', async () => {1425 function PresetPrecedence() {1426 ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});1427 }1428 await act(() => {1429 const {pipe} = renderToPipeableStream(1430 <html>1431 <head />1432 <body>1433 <link rel="stylesheet" href="initial" precedence="one" />1434 <PresetPrecedence />1435 <div>1436 <Suspense fallback="loading foo bar...">1437 <div>foo</div>1438 <link rel="stylesheet" href="foo" precedence="one" />1439 <BlockedOn value="bar">1440 <div>bar</div>1441 <link rel="stylesheet" href="bar" precedence="default" />1442 </BlockedOn>1443 </Suspense>1444 </div>1445 <div>1446 <Suspense fallback="loading bar baz qux...">1447 <BlockedOn value="bar">1448 <div>bar</div>1449 <link rel="stylesheet" href="bar" precedence="default" />1450 </BlockedOn>1451 <BlockedOn value="baz">1452 <div>baz</div>1453 <link rel="stylesheet" href="baz" precedence="two" />1454 </BlockedOn>1455 <BlockedOn value="qux">1456 <div>qux</div>1457 <link rel="stylesheet" href="qux" precedence="one" />1458 </BlockedOn>1459 </Suspense>1460 </div>1461 <div>1462 <Suspense fallback="loading bar baz qux...">1463 <BlockedOn value="unblock">1464 <BlockedOn value="bar">1465 <div>bar</div>1466 <link rel="stylesheet" href="bar" precedence="default" />1467 </BlockedOn>1468 <BlockedOn value="baz">1469 <div>baz</div>1470 <link rel="stylesheet" href="baz" precedence="two" />1471 </BlockedOn>1472 <BlockedOn value="qux">1473 <div>qux</div>1474 <link rel="stylesheet" href="qux" precedence="one" />1475 </BlockedOn>1476 </BlockedOn>1477 </Suspense>1478 </div>1479 </body>1480 </html>,1481 );1482 pipe(writable);1483 });14841485 expect(getMeaningfulChildren(document)).toEqual(1486 <html>1487 <head>1488 <link rel="stylesheet" href="initial" data-precedence="one" />1489 <link rel="stylesheet" href="foo" data-precedence="one" />1490 <link rel="stylesheet" href="preset" data-precedence="preset" />1491 </head>1492 <body>1493 <div>loading foo bar...</div>1494 <div>loading bar baz qux...</div>1495 <div>loading bar baz qux...</div>1496 </body>1497 </html>,1498 );14991500 await act(() => {1501 resolveText('foo');1502 resolveText('bar');1503 });15041505 expect(getMeaningfulChildren(document)).toEqual(1506 <html>1507 <head>1508 <link rel="stylesheet" href="initial" data-precedence="one" />1509 <link rel="stylesheet" href="foo" data-precedence="one" />1510 <link rel="stylesheet" href="preset" data-precedence="preset" />1511 <link rel="stylesheet" href="bar" data-precedence="default" />1512 </head>1513 <body>1514 <div>loading foo bar...</div>1515 <div>loading bar baz qux...</div>1516 <div>loading bar baz qux...</div>1517 <link rel="preload" href="bar" as="style" />1518 </body>1519 </html>,1520 );15211522 await act(() => {1523 const link = document.querySelector('link[rel="stylesheet"][href="foo"]');1524 const event = document.createEvent('Events');1525 event.initEvent('load', true, true);1526 link.dispatchEvent(event);1527 });15281529 expect(getMeaningfulChildren(document)).toEqual(1530 <html>1531 <head>1532 <link rel="stylesheet" href="initial" data-precedence="one" />1533 <link rel="stylesheet" href="foo" data-precedence="one" />1534 <link rel="stylesheet" href="preset" data-precedence="preset" />1535 <link rel="stylesheet" href="bar" data-precedence="default" />1536 </head>1537 <body>1538 <div>loading foo bar...</div>1539 <div>loading bar baz qux...</div>1540 <div>loading bar baz qux...</div>1541 <link rel="preload" href="bar" as="style" />1542 </body>1543 </html>,1544 );15451546 await act(() => {1547 const link = document.querySelector('link[rel="stylesheet"][href="bar"]');1548 const event = document.createEvent('Events');1549 event.initEvent('load', true, true);1550 link.dispatchEvent(event);1551 });15521553 expect(getMeaningfulChildren(document)).toEqual(1554 <html>1555 <head>1556 <link rel="stylesheet" href="initial" data-precedence="one" />1557 <link rel="stylesheet" href="foo" data-precedence="one" />1558 <link rel="stylesheet" href="preset" data-precedence="preset" />1559 <link rel="stylesheet" href="bar" data-precedence="default" />1560 </head>1561 <body>1562 <div>1563 <div>foo</div>1564 <div>bar</div>1565 </div>1566 <div>loading bar baz qux...</div>1567 <div>loading bar baz qux...</div>1568 <link rel="preload" href="bar" as="style" />1569 </body>1570 </html>,1571 );15721573 await act(() => {1574 resolveText('baz');1575 });15761577 expect(getMeaningfulChildren(document)).toEqual(1578 <html>1579 <head>1580 <link rel="stylesheet" href="initial" data-precedence="one" />1581 <link rel="stylesheet" href="foo" data-precedence="one" />1582 <link rel="stylesheet" href="preset" data-precedence="preset" />1583 <link rel="stylesheet" href="bar" data-precedence="default" />1584 </head>1585 <body>1586 <div>1587 <div>foo</div>1588 <div>bar</div>1589 </div>1590 <div>loading bar baz qux...</div>1591 <div>loading bar baz qux...</div>1592 <link rel="preload" as="style" href="bar" />1593 <link rel="preload" as="style" href="baz" />1594 </body>1595 </html>,1596 );15971598 await act(() => {1599 resolveText('qux');1600 });16011602 expect(getMeaningfulChildren(document)).toEqual(1603 <html>1604 <head>1605 <link rel="stylesheet" href="initial" data-precedence="one" />1606 <link rel="stylesheet" href="foo" data-precedence="one" />1607 <link rel="stylesheet" href="qux" data-precedence="one" />1608 <link rel="stylesheet" href="preset" data-precedence="preset" />1609 <link rel="stylesheet" href="bar" data-precedence="default" />1610 <link rel="stylesheet" href="baz" data-precedence="two" />1611 </head>1612 <body>1613 <div>1614 <div>foo</div>1615 <div>bar</div>1616 </div>1617 <div>loading bar baz qux...</div>1618 <div>loading bar baz qux...</div>1619 <link rel="preload" as="style" href="bar" />1620 <link rel="preload" as="style" href="baz" />1621 <link rel="preload" as="style" href="qux" />1622 </body>1623 </html>,1624 );16251626 await act(() => {1627 const bazlink = document.querySelector(1628 'link[rel="stylesheet"][href="baz"]',1629 );1630 const quxlink = document.querySelector(1631 'link[rel="stylesheet"][href="qux"]',1632 );1633 const presetLink = document.querySelector(1634 'link[rel="stylesheet"][href="preset"]',1635 );1636 const event = document.createEvent('Events');1637 event.initEvent('load', true, true);1638 bazlink.dispatchEvent(event);1639 quxlink.dispatchEvent(event);1640 presetLink.dispatchEvent(event);1641 });16421643 expect(getMeaningfulChildren(document)).toEqual(1644 <html>1645 <head>1646 <link rel="stylesheet" href="initial" data-precedence="one" />1647 <link rel="stylesheet" href="foo" data-precedence="one" />1648 <link rel="stylesheet" href="qux" data-precedence="one" />1649 <link rel="stylesheet" href="preset" data-precedence="preset" />1650 <link rel="stylesheet" href="bar" data-precedence="default" />1651 <link rel="stylesheet" href="baz" data-precedence="two" />1652 </head>1653 <body>1654 <div>1655 <div>foo</div>1656 <div>bar</div>1657 </div>1658 <div>1659 <div>bar</div>1660 <div>baz</div>1661 <div>qux</div>1662 </div>1663 <div>loading bar baz qux...</div>1664 <link rel="preload" as="style" href="bar" />1665 <link rel="preload" as="style" href="baz" />1666 <link rel="preload" as="style" href="qux" />1667 </body>1668 </html>,1669 );16701671 await act(() => {1672 resolveText('unblock');1673 });16741675 expect(getMeaningfulChildren(document)).toEqual(1676 <html>1677 <head>1678 <link rel="stylesheet" href="initial" data-precedence="one" />1679 <link rel="stylesheet" href="foo" data-precedence="one" />1680 <link rel="stylesheet" href="qux" data-precedence="one" />1681 <link rel="stylesheet" href="preset" data-precedence="preset" />1682 <link rel="stylesheet" href="bar" data-precedence="default" />1683 <link rel="stylesheet" href="baz" data-precedence="two" />1684 </head>1685 <body>1686 <div>1687 <div>foo</div>1688 <div>bar</div>1689 </div>1690 <div>1691 <div>bar</div>1692 <div>baz</div>1693 <div>qux</div>1694 </div>1695 <div>1696 <div>bar</div>1697 <div>baz</div>1698 <div>qux</div>1699 </div>1700 <link rel="preload" as="style" href="bar" />1701 <link rel="preload" as="style" href="baz" />1702 <link rel="preload" as="style" href="qux" />1703 </body>1704 </html>,1705 );1706 });17071708 it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => {1709 await act(() => {1710 const {pipe} = renderToPipeableStream(1711 <html>1712 <head />1713 <body>1714 <div>1715 outer1716 <link rel="stylesheet" href="1one" precedence="one" />1717 <link rel="stylesheet" href="1two" precedence="two" />1718 <link rel="stylesheet" href="1three" precedence="three" />1719 <link rel="stylesheet" href="1four" precedence="four" />1720 <Suspense fallback={null}>1721 <div>1722 middle1723 <link rel="stylesheet" href="2one" precedence="one" />1724 <link rel="stylesheet" href="2two" precedence="two" />1725 <link rel="stylesheet" href="2three" precedence="three" />1726 <link rel="stylesheet" href="2four" precedence="four" />1727 <Suspense fallback={null}>1728 <div>1729 inner1730 <link rel="stylesheet" href="3five" precedence="five" />1731 <link rel="stylesheet" href="3one" precedence="one" />1732 <link rel="stylesheet" href="3two" precedence="two" />1733 <link rel="stylesheet" href="3three" precedence="three" />1734 <link rel="stylesheet" href="3four" precedence="four" />1735 </div>1736 </Suspense>1737 </div>1738 </Suspense>1739 <Suspense fallback={null}>1740 <div>middle</div>1741 <link rel="stylesheet" href="4one" precedence="one" />1742 <link rel="stylesheet" href="4two" precedence="two" />1743 <link rel="stylesheet" href="4three" precedence="three" />1744 <link rel="stylesheet" href="4four" precedence="four" />1745 </Suspense>1746 </div>1747 </body>1748 </html>,1749 );1750 pipe(writable);1751 });17521753 expect(getMeaningfulChildren(document)).toEqual(1754 <html>1755 <head>1756 <link rel="stylesheet" href="1one" data-precedence="one" />1757 <link rel="stylesheet" href="2one" data-precedence="one" />1758 <link rel="stylesheet" href="3one" data-precedence="one" />1759 <link rel="stylesheet" href="4one" data-precedence="one" />17601761 <link rel="stylesheet" href="1two" data-precedence="two" />1762 <link rel="stylesheet" href="2two" data-precedence="two" />1763 <link rel="stylesheet" href="3two" data-precedence="two" />1764 <link rel="stylesheet" href="4two" data-precedence="two" />17651766 <link rel="stylesheet" href="1three" data-precedence="three" />1767 <link rel="stylesheet" href="2three" data-precedence="three" />1768 <link rel="stylesheet" href="3three" data-precedence="three" />1769 <link rel="stylesheet" href="4three" data-precedence="three" />17701771 <link rel="stylesheet" href="1four" data-precedence="four" />1772 <link rel="stylesheet" href="2four" data-precedence="four" />1773 <link rel="stylesheet" href="3four" data-precedence="four" />1774 <link rel="stylesheet" href="4four" data-precedence="four" />17751776 <link rel="stylesheet" href="3five" data-precedence="five" />1777 </head>1778 <body>1779 <div>1780 outer1781 <div>1782 middle<div>inner</div>1783 </div>1784 <div>middle</div>1785 </div>1786 </body>1787 </html>,1788 );1789 });17901791 it('stylesheet resources are inserted according to precedence order on the client', async () => {1792 await act(() => {1793 const {pipe} = renderToPipeableStream(1794 <html>1795 <head />1796 <body>1797 <div>1798 <link rel="stylesheet" href="foo" precedence="one" />1799 <link rel="stylesheet" href="bar" precedence="two" />1800 Hello1801 </div>1802 </body>1803 </html>,1804 );1805 pipe(writable);1806 });18071808 expect(getMeaningfulChildren(document)).toEqual(1809 <html>1810 <head>1811 <link rel="stylesheet" href="foo" data-precedence="one" />1812 <link rel="stylesheet" href="bar" data-precedence="two" />1813 </head>1814 <body>1815 <div>Hello</div>1816 </body>1817 </html>,1818 );18191820 const root = ReactDOMClient.hydrateRoot(1821 document,1822 <html>1823 <head />1824 <body>1825 <div>1826 <link rel="stylesheet" href="foo" precedence="one" />1827 <link rel="stylesheet" href="bar" precedence="two" />1828 Hello1829 </div>1830 </body>1831 </html>,1832 );1833 await waitForAll([]);1834 expect(getMeaningfulChildren(document)).toEqual(1835 <html>1836 <head>1837 <link rel="stylesheet" href="foo" data-precedence="one" />1838 <link rel="stylesheet" href="bar" data-precedence="two" />1839 </head>1840 <body>1841 <div>Hello</div>1842 </body>1843 </html>,1844 );18451846 root.render(1847 <html>1848 <head />1849 <body>1850 <div>Goodbye</div>1851 <link rel="stylesheet" href="baz" precedence="one" />1852 </body>1853 </html>,1854 );1855 await waitForAll([]);1856 await act(() => {1857 loadPreloads();1858 loadStylesheets();1859 });1860 await assertLog([1861 'load preload: baz',1862 'load stylesheet: foo',1863 'load stylesheet: baz',1864 'load stylesheet: bar',1865 ]);1866 expect(getMeaningfulChildren(document)).toEqual(1867 <html>1868 <head>1869 <link rel="stylesheet" href="foo" data-precedence="one" />1870 <link rel="stylesheet" href="baz" data-precedence="one" />1871 <link rel="stylesheet" href="bar" data-precedence="two" />1872 <link rel="preload" as="style" href="baz" />1873 </head>1874 <body>1875 <div>Goodbye</div>1876 </body>1877 </html>,1878 );1879 });18801881 it('inserts preloads in render phase eagerly', async () => {1882 function Throw() {1883 throw new Error('Uh oh!');1884 }1885 class ErrorBoundary extends React.Component {1886 state = {hasError: false, error: null};1887 static getDerivedStateFromError(error) {1888 return {1889 hasError: true,1890 error,1891 };1892 }1893 render() {1894 if (this.state.hasError) {1895 return this.state.error.message;1896 }1897 return this.props.children;1898 }1899 }19001901 const root = ReactDOMClient.createRoot(container);1902 root.render(1903 <ErrorBoundary>1904 <link rel="stylesheet" href="foo" precedence="default" />1905 <div>foo</div>1906 <Throw />1907 </ErrorBoundary>,1908 );1909 await waitForAll([]);1910 expect(getMeaningfulChildren(document)).toEqual(1911 <html>1912 <head>1913 <link rel="preload" href="foo" as="style" />1914 </head>1915 <body>1916 <div id="container">Uh oh!</div>1917 </body>1918 </html>,1919 );1920 });19211922 it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => {1923 await act(() => {1924 const {pipe} = renderToPipeableStream(1925 <html>1926 <head />1927 <body>1928 <div>1929 <Suspense fallback="loading foo...">1930 <BlockedOn value="foo">1931 <div>foo</div>1932 <link rel="stylesheet" href="foo" precedence="default" />1933 <Suspense fallback="loading bar...">1934 <BlockedOn value="bar">1935 <div>bar</div>1936 <link rel="stylesheet" href="bar" precedence="default" />1937 <Suspense fallback="loading baz...">1938 <BlockedOn value="baz">1939 <div>baz</div>1940 <link1941 rel="stylesheet"1942 href="baz"1943 precedence="default"1944 />1945 </BlockedOn>1946 </Suspense>1947 </BlockedOn>1948 </Suspense>1949 </BlockedOn>1950 </Suspense>1951 </div>1952 </body>1953 </html>,1954 );1955 pipe(writable);1956 });19571958 expect(getMeaningfulChildren(document)).toEqual(1959 <html>1960 <head />1961 <body>1962 <div>loading foo...</div>1963 </body>1964 </html>,1965 );19661967 await act(() => {1968 resolveText('bar');1969 });1970 expect(getMeaningfulChildren(document)).toEqual(1971 <html>1972 <head />1973 <body>1974 <div>loading foo...</div>1975 </body>1976 </html>,1977 );19781979 await act(() => {1980 resolveText('baz');1981 });1982 expect(getMeaningfulChildren(document)).toEqual(1983 <html>1984 <head />1985 <body>1986 <div>loading foo...</div>1987 </body>1988 </html>,1989 );19901991 await act(() => {1992 resolveText('foo');1993 });1994 expect(getMeaningfulChildren(document)).toEqual(1995 <html>1996 <head>1997 <link rel="stylesheet" href="foo" data-precedence="default" />1998 <link rel="stylesheet" href="bar" data-precedence="default" />1999 <link rel="stylesheet" href="baz" data-precedence="default" />2000 </head>
Findings
✓ No findings reported for this file.