main
js 10,364 lines 322 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12 import {
13 insertNodesAndExecuteScripts,
14 mergeOptions,
15 } from '../test-utils/FizzTestUtils';
16
17 let JSDOM;
18 let Stream;
19 let React;
20 let ReactDOM;
21 let ReactDOMClient;
22 let ReactDOMFizzServer;
23 let Suspense;
24 let SuspenseList;
25 let textCache;
26 let loadCache;
27 let writable;
28 let CSPnonce = null;
29 let container;
30 let buffer = '';
31 let hasErrored = false;
32 let fatalError = undefined;
33 let renderOptions;
34 let waitForAll;
35 let assertLog;
36 let Scheduler;
37 let clientAct;
38 let streamingContainer;
39 let assertConsoleErrorDev;
40
41 describe('ReactDOMFloat', () => {
42 beforeEach(() => {
43 jest.resetModules();
44 JSDOM = require('jsdom').JSDOM;
45
46 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 else
53 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');
71
72 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');
81
82 const InternalTestUtils = require('internal-test-utils');
83 waitForAll = InternalTestUtils.waitForAll;
84 assertLog = InternalTestUtils.assertLog;
85 clientAct = InternalTestUtils.act;
86 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
87
88 textCache = new Map();
89 loadCache = new Set();
90
91 buffer = '';
92 hasErrored = false;
93
94 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 });
103
104 renderOptions = {};
105 if (gate(flags => flags.shouldUseFizzExternalRuntime)) {
106 renderOptions.unstable_externalRuntimeSrc =
107 'react-dom/unstable_server-external-runtime';
108 }
109 });
110
111 const bodyStartMatch = /<body(?:>| .*?>)/;
112 const headStartMatch = /<head(?:>| .*?>)/;
113
114 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 = '';
129
130 if (!bufferedContent) {
131 jest.runAllTimers();
132 return;
133 }
134
135 const bodyMatch = bufferedContent.match(bodyStartMatch);
136 const headMatch = bufferedContent.match(headStartMatch);
137
138 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 the
141 // container. This is not really production behavior because you can't correctly stream into a deep div effectively
142 // but it's pragmatic for tests.
143
144 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 process
151 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 }
160
161 if (bufferedContent.startsWith('<!DOCTYPE html>')) {
162 // we can just use the whole document
163 const tempDom = new JSDOM(bufferedContent);
164
165 // Wipe existing head and body content
166 document.head.innerHTML = '';
167 document.body.innerHTML = '';
168
169 // Copy the <html> attributes over
170 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 }
175
176 if (headMatch) {
177 // We parsed a head open tag. we need to copy head attributes and insert future
178 // 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 }
189
190 if (bodyMatch) {
191 // We parsed a body open tag. we need to copy head attributes and insert future
192 // 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 }
203
204 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);
217
218 const tempHeadNode = tempDom.window.document.head;
219 const source = document.createElement('head');
220 source.innerHTML = tempHeadNode.innerHTML;
221 await insertNodesAndExecuteScripts(source, document.head, CSPnonce);
222
223 if (bodyMatch) {
224 streamingContainer = document.body;
225
226 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 reveal
242 jest.runAllTimers();
243 }
244
245 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 children
252 // so we have a global override where if this data attribute is included we also include the node
253 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 expect
265 (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 === 0
289 ? undefined
290 : children.length === 1
291 ? children[0]
292 : children;
293 }
294
295 function BlockedOn({value, children}) {
296 readText(value);
297 return children;
298 }
299
300 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 }
315
316 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 };
338
339 const newRecord = {
340 status: 'pending',
341 value: thenable,
342 };
343 textCache.set(text, newRecord);
344
345 throw thenable;
346 }
347 }
348
349 function AsyncText({text}) {
350 return readText(text);
351 }
352
353 function renderToPipeableStream(jsx, options) {
354 // Merge options with renderOptions, which may contain featureFlag specific behavior
355 return ReactDOMFizzServer.renderToPipeableStream(
356 jsx,
357 mergeOptions(options, renderOptions),
358 );
359 }
360
361 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 }
368
369 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 }
376
377 function loadStylesheets(hrefs) {
378 loadStylesheetsFrom(document, hrefs);
379 }
380
381 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 }
388
389 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 }
396
397 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 }
412
413 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 need
430 // to clear the scheduler first otherwise the expected failure will fail
431 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 });
444
445 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>&lt;link rel="icon" href="icon"&gt;</noscript>
471 <base target="foo" href="bar" />
472 <script async="" src="foo" />
473 </head>
474 <body>foo</body>
475 </html>,
476 );
477
478 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>&lt;link rel="icon" href="icon"&gt;</noscript>
502 <base target="foo" href="bar" />
503 <script async="" src="foo" />
504 </head>
505 <body>foo</body>
506 </html>,
507 );
508 });
509
510 it('warns if you render resource-like elements above <head> or <body>', async () => {
511 const root = ReactDOMClient.createRoot(document);
512
513 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 ]);
525
526 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 ]);
544
545 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 ]);
569
570 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 ]);
583
584 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 ]);
614
615 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 ]);
628
629 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 });
645
646 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 );
664
665 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 reconstructed
673 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 });
684
685 it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {
686 const chunks = [];
687
688 writable.on('data', chunk => {
689 chunks.push(chunk);
690 });
691
692 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 });
720
721 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 }
733
734 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 });
748
749 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 });
755
756 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 bar
768 </BlockedOn>
769 </Suspense>
770 </body>
771 </html>,
772 ).pipe(writable);
773 });
774
775 expect(getMeaningfulChildren(document)).toEqual(
776 <html>
777 <head />
778 <body>
779 {'loading foo...'}
780 {'loading bar...'}
781 </body>
782 </html>,
783 );
784
785 await act(() => {
786 resolveText('foo');
787 });
788 expect(getMeaningfulChildren(document)).toEqual(
789 <html>
790 <head />
791 <body>
792 foo
793 {'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 foo
807 {'loading bar...'}
808 <link rel="preload" href="bar" as="style" />
809 </body>
810 </html>,
811 );
812 });
813
814 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 foo
823 </BlockedOn>
824 </Suspense>
825 <Suspense fallback="loading bar...">
826 <BlockedOn value="bar">
827 <link rel="stylesheet" href="bar" precedence="bar" />
828 bar
829 </BlockedOn>
830 </Suspense>
831 </body>
832 </html>,
833 ).pipe(writable);
834 });
835
836 expect(getMeaningfulChildren(document)).toEqual(
837 <html>
838 <head />
839 <body>
840 {'loading foo...'}
841 {'loading bar...'}
842 </body>
843 </html>,
844 );
845
846 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 foo
854 </Suspense>
855 <Suspense fallback="loading bar...">
856 <link rel="stylesheet" href="bar" precedence="bar" />
857 bar
858 </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 );
877
878 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 );
899
900 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 });
923
924 it('can hoist <link rel="stylesheet" .../> and <style /> tags together, respecting order of discovery', async () => {
925 const css = `
926 body {
927 background-color: red;
928 }`;
929
930 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 });
987
988 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 );
1003
1004 await act(() => {
1005 resolveText('block');
1006 });
1007
1008 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 );
1036
1037 await act(() => {
1038 resolveText('block2');
1039 });
1040
1041 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 );
1082
1083 await act(() => {
1084 resolveText('block again');
1085 });
1086
1087 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 );
1128
1129 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 ]);
1173
1174 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 });
1231
1232 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 Hello
1243 </BlockedOn>
1244 </Suspense>
1245 </body>
1246 </html>
1247 );
1248 }
1249 await act(() => {
1250 const {pipe} = renderToPipeableStream(<App />);
1251 pipe(writable);
1252 });
1253
1254 expect(getMeaningfulChildren(document)).toEqual(
1255 <html>
1256 <head />
1257 <body>loading...</body>
1258 </html>,
1259 );
1260
1261 await act(() => {
1262 resolveText('unblock');
1263 });
1264
1265 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 );
1278
1279 errorStylesheets(['bar']);
1280 assertLog(['error stylesheet: bar']);
1281
1282 await waitForAll([]);
1283
1284 const boundaryTemplateInstance = document.getElementById('B:0');
1285 const suspenseInstance = boundaryTemplateInstance.previousSibling;
1286
1287 expect(suspenseInstance.data).toEqual('$!');
1288 expect(boundaryTemplateInstance.dataset.dgst).toBe('CSS failed to load');
1289
1290 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 );
1303
1304 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 promise
1313 // We need to use that promise to resolve the suspended commit because we don't know if the load or error
1314 // events have already fired. This requires the load to be awaited for the commit to have a chance to flush
1315 // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar
1316 // 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 Hello
1329 </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 });
1337
1338 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 Hello
1346 </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 );
1359
1360 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 });
1377
1378 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 sometimes
1380 // because the link gets elided as a resource
1381 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 }
1390
1391 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 });
1407
1408 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 });
1423
1424 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 });
1484
1485 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 );
1499
1500 await act(() => {
1501 resolveText('foo');
1502 resolveText('bar');
1503 });
1504
1505 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 );
1521
1522 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 });
1528
1529 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 );
1545
1546 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 });
1552
1553 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 );
1572
1573 await act(() => {
1574 resolveText('baz');
1575 });
1576
1577 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 );
1597
1598 await act(() => {
1599 resolveText('qux');
1600 });
1601
1602 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 );
1625
1626 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 });
1642
1643 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 );
1670
1671 await act(() => {
1672 resolveText('unblock');
1673 });
1674
1675 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 });
1707
1708 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 outer
1716 <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 middle
1723 <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 inner
1730 <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 });
1752
1753 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" />
1760
1761 <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" />
1765
1766 <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" />
1770
1771 <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" />
1775
1776 <link rel="stylesheet" href="3five" data-precedence="five" />
1777 </head>
1778 <body>
1779 <div>
1780 outer
1781 <div>
1782 middle<div>inner</div>
1783 </div>
1784 <div>middle</div>
1785 </div>
1786 </body>
1787 </html>,
1788 );
1789 });
1790
1791 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 Hello
1801 </div>
1802 </body>
1803 </html>,
1804 );
1805 pipe(writable);
1806 });
1807
1808 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 );
1819
1820 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 Hello
1829 </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 );
1845
1846 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 });
1880
1881 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 }
1900
1901 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 });
1921
1922 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 <link
1941 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 });
1957
1958 expect(getMeaningfulChildren(document)).toEqual(
1959 <html>
1960 <head />
1961 <body>
1962 <div>loading foo...</div>
1963 </body>
1964 </html>,
1965 );
1966
1967 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 );
1978
1979 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 );
1990
1991 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>
2001 <body>
2002 <div>loading foo...</div>
2003 <link rel="preload" href="foo" as="style" />
2004 <link rel="preload" href="bar" as="style" />
2005 <link rel="preload" href="baz" as="style" />
2006 </body>
2007 </html>,
2008 );
2009
2010 await act(() => {
2011 const event = document.createEvent('Events');
2012 event.initEvent('load', true, true);
2013 Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
2014 el => {
2015 el.dispatchEvent(event);
2016 },
2017 );
2018 });
2019 expect(getMeaningfulChildren(document)).toEqual(
2020 <html>
2021 <head>
2022 <link rel="stylesheet" href="foo" data-precedence="default" />
2023 <link rel="stylesheet" href="bar" data-precedence="default" />
2024 <link rel="stylesheet" href="baz" data-precedence="default" />
2025 </head>
2026 <body>
2027 <div>
2028 <div>foo</div>
2029 <div>bar</div>
2030 <div>baz</div>
2031 </div>
2032 <link rel="preload" href="foo" as="style" />
2033 <link rel="preload" href="bar" as="style" />
2034 <link rel="preload" href="baz" as="style" />
2035 </body>
2036 </html>,
2037 );
2038 });
2039
2040 it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
2041 await act(() => {
2042 const {pipe} = renderToPipeableStream(
2043 <html>
2044 <head />
2045 <body>
2046 <div>
2047 <Suspense fallback="loading...">
2048 <div>
2049 <BlockedOn value="foo">
2050 <div>foo</div>
2051 <link rel="stylesheet" href="foo" precedence="default" />
2052 <Suspense fallback="loading bar...">
2053 <BlockedOn value="bar">
2054 <div>bar</div>
2055 <link
2056 rel="stylesheet"
2057 href="bar"
2058 precedence="default"
2059 />
2060 <Suspense fallback="loading baz...">
2061 <div>
2062 <BlockedOn value="baz">
2063 <div>baz</div>
2064 <link
2065 rel="stylesheet"
2066 href="baz"
2067 precedence="default"
2068 />
2069 </BlockedOn>
2070 </div>
2071 </Suspense>
2072 </BlockedOn>
2073 </Suspense>
2074 </BlockedOn>
2075 <BlockedOn value="qux">
2076 <div>qux</div>
2077 <link rel="stylesheet" href="qux" precedence="default" />
2078 </BlockedOn>
2079 </div>
2080 </Suspense>
2081 </div>
2082 </body>
2083 </html>,
2084 );
2085 pipe(writable);
2086 });
2087
2088 expect(getMeaningfulChildren(document)).toEqual(
2089 <html>
2090 <head />
2091 <body>
2092 <div>loading...</div>
2093 </body>
2094 </html>,
2095 );
2096
2097 // This will enqueue a stylesheet resource in a deep blocked boundary (loading baz...).
2098 await act(() => {
2099 resolveText('baz');
2100 });
2101 expect(getMeaningfulChildren(document)).toEqual(
2102 <html>
2103 <head />
2104 <body>
2105 <div>loading...</div>
2106 </body>
2107 </html>,
2108 );
2109
2110 // This will enqueue a stylesheet resource in the intermediate blocked boundary (loading bar...).
2111 await act(() => {
2112 resolveText('bar');
2113 });
2114 expect(getMeaningfulChildren(document)).toEqual(
2115 <html>
2116 <head />
2117 <body>
2118 <div>loading...</div>
2119 </body>
2120 </html>,
2121 );
2122
2123 // This will complete a segment in the top level boundary that is still blocked on another segment.
2124 // It will flush the completed segment however the inner boundaries should not emit their style dependencies
2125 // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked
2126 // boundary (top level).
2127 await act(() => {
2128 resolveText('foo');
2129 });
2130 expect(getMeaningfulChildren(document)).toEqual(
2131 <html>
2132 <head />
2133 <body>
2134 <div>loading...</div>
2135 <link rel="preload" href="foo" as="style" />
2136 <link rel="preload" href="bar" as="style" />
2137 <link rel="preload" href="baz" as="style" />
2138 </body>
2139 </html>,
2140 );
2141
2142 // This resolves the last blocked segment on the top level boundary so we see all dependencies of the
2143 // nested boundaries emitted at this level
2144 await act(() => {
2145 resolveText('qux');
2146 });
2147 expect(getMeaningfulChildren(document)).toEqual(
2148 <html>
2149 <head>
2150 <link rel="stylesheet" href="foo" data-precedence="default" />
2151 <link rel="stylesheet" href="bar" data-precedence="default" />
2152 <link rel="stylesheet" href="baz" data-precedence="default" />
2153 <link rel="stylesheet" href="qux" data-precedence="default" />
2154 </head>
2155 <body>
2156 <div>loading...</div>
2157 <link rel="preload" href="foo" as="style" />
2158 <link rel="preload" href="bar" as="style" />
2159 <link rel="preload" href="baz" as="style" />
2160 <link rel="preload" href="qux" as="style" />
2161 </body>
2162 </html>,
2163 );
2164
2165 // We load all stylesheets and confirm the content is revealed
2166 await act(() => {
2167 const event = document.createEvent('Events');
2168 event.initEvent('load', true, true);
2169 Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
2170 el => {
2171 el.dispatchEvent(event);
2172 },
2173 );
2174 });
2175 expect(getMeaningfulChildren(document)).toEqual(
2176 <html>
2177 <head>
2178 <link rel="stylesheet" href="foo" data-precedence="default" />
2179 <link rel="stylesheet" href="bar" data-precedence="default" />
2180 <link rel="stylesheet" href="baz" data-precedence="default" />
2181 <link rel="stylesheet" href="qux" data-precedence="default" />
2182 </head>
2183 <body>
2184 <div>
2185 <div>
2186 <div>foo</div>
2187 <div>bar</div>
2188 <div>
2189 <div>baz</div>
2190 </div>
2191 <div>qux</div>
2192 </div>
2193 </div>
2194 <link rel="preload" href="foo" as="style" />
2195 <link rel="preload" href="bar" as="style" />
2196 <link rel="preload" href="baz" as="style" />
2197 <link rel="preload" href="qux" as="style" />
2198 </body>
2199 </html>,
2200 );
2201 });
2202
2203 it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => {
2204 function App() {
2205 return (
2206 <html>
2207 <head />
2208 <body>
2209 <div>
2210 <link
2211 // This preload is explicit so it can flush with a lot of potential attrs
2212 // We will duplicate this as a style that flushes after the shell
2213 rel="stylesheet"
2214 href="foo"
2215 // precedence is not a special attribute for preloads so this will just flush as is
2216 precedence="default"
2217 // Some standard link props
2218 crossOrigin="anonymous"
2219 media="all"
2220 integrity="somehash"
2221 referrerPolicy="origin"
2222 // data and non starndard attributes that should flush
2223 data-foo={'"quoted"'}
2224 nonStandardAttr="attr"
2225 properlyformattednonstandardattr="attr"
2226 // attributes that should be filtered out for violating certain rules
2227 onSomething="this should be removed b/c event handler"
2228 shouldnotincludefunctions={() => {}}
2229 norsymbols={Symbol('foo')}
2230 />
2231 <Suspense fallback={'loading...'}>
2232 <BlockedOn value="unblock">
2233 <link
2234 // This preload is explicit so it can flush with a lot of potential attrs
2235 // We will duplicate this as a style that flushes after the shell
2236 rel="stylesheet"
2237 href="bar"
2238 // opt-in property to get this treated as a resource
2239 precedence="default"
2240 // Some standard link props
2241 crossOrigin="anonymous"
2242 media="all"
2243 integrity="somehash"
2244 referrerPolicy="origin"
2245 // data and non starndard attributes that should flush
2246 data-foo={'"quoted"'}
2247 nonStandardAttr="attr"
2248 properlyformattednonstandardattr="attr"
2249 // attributes that should be filtered out for violating certain rules
2250 onSomething="this should be removed b/c event handler"
2251 shouldnotincludefunctions={() => {}}
2252 norsymbols={Symbol('foo')}
2253 />
2254 </BlockedOn>
2255 </Suspense>
2256 </div>
2257 </body>
2258 </html>
2259 );
2260 }
2261 await act(() => {
2262 const {pipe} = renderToPipeableStream(<App />);
2263 pipe(writable);
2264 });
2265 expect(getMeaningfulChildren(document)).toEqual(
2266 <html>
2267 <head>
2268 <link
2269 rel="stylesheet"
2270 href="foo"
2271 data-precedence="default"
2272 crossorigin="anonymous"
2273 media="all"
2274 integrity="somehash"
2275 referrerpolicy="origin"
2276 data-foo={'"quoted"'}
2277 nonstandardattr="attr"
2278 properlyformattednonstandardattr="attr"
2279 />
2280 </head>
2281 <body>
2282 <div>loading...</div>
2283 </body>
2284 </html>,
2285 );
2286 assertConsoleErrorDev([
2287 'React does not recognize the `nonStandardAttr` prop on a DOM element. ' +
2288 'If you intentionally want it to appear in the DOM as a custom attribute, ' +
2289 'spell it as lowercase `nonstandardattr` instead. If you accidentally passed it from a ' +
2290 'parent component, remove it from the DOM element.\n' +
2291 ' in link (at **)\n' +
2292 ' in App (at **)',
2293 'Invalid values for props `shouldnotincludefunctions`, `norsymbols` on <link> tag. ' +
2294 'Either remove them from the element, or pass a string or number value to keep them in the DOM. ' +
2295 'For details, see https://react.dev/link/attribute-behavior \n' +
2296 ' in link (at **)\n' +
2297 ' in App (at **)',
2298 ]);
2299
2300 // Now we flush the stylesheet with the boundary
2301 await act(() => {
2302 resolveText('unblock');
2303 });
2304
2305 expect(getMeaningfulChildren(document)).toEqual(
2306 <html>
2307 <head>
2308 <link
2309 rel="stylesheet"
2310 href="foo"
2311 data-precedence="default"
2312 crossorigin="anonymous"
2313 media="all"
2314 integrity="somehash"
2315 referrerpolicy="origin"
2316 data-foo={'"quoted"'}
2317 nonstandardattr="attr"
2318 properlyformattednonstandardattr="attr"
2319 />
2320 <link
2321 rel="stylesheet"
2322 href="bar"
2323 data-precedence="default"
2324 crossorigin="anonymous"
2325 media="all"
2326 integrity="somehash"
2327 referrerpolicy="origin"
2328 data-foo={'"quoted"'}
2329 nonstandardattr="attr"
2330 properlyformattednonstandardattr="attr"
2331 />
2332 </head>
2333 <body>
2334 <div>loading...</div>
2335 <link
2336 rel="preload"
2337 as="style"
2338 href="bar"
2339 crossorigin="anonymous"
2340 media="all"
2341 integrity="somehash"
2342 referrerpolicy="origin"
2343 />
2344 </body>
2345 </html>,
2346 );
2347 });
2348
2349 it('boundary stylesheet resource dependencies hoist to a parent boundary when flushed inline', async () => {
2350 await act(() => {
2351 const {pipe} = renderToPipeableStream(
2352 <html>
2353 <head />
2354 <body>
2355 <div>
2356 <Suspense fallback="loading A...">
2357 <BlockedOn value="unblock">
2358 <AsyncText text="A" />
2359 <link rel="stylesheet" href="A" precedence="A" />
2360 <Suspense fallback="loading AA...">
2361 <AsyncText text="AA" />
2362 <link rel="stylesheet" href="AA" precedence="AA" />
2363 <Suspense fallback="loading AAA...">
2364 <AsyncText text="AAA" />
2365 <link rel="stylesheet" href="AAA" precedence="AAA" />
2366 <Suspense fallback="loading AAAA...">
2367 <AsyncText text="AAAA" />
2368 <link rel="stylesheet" href="AAAA" precedence="AAAA" />
2369 </Suspense>
2370 </Suspense>
2371 </Suspense>
2372 </BlockedOn>
2373 </Suspense>
2374 </div>
2375 </body>
2376 </html>,
2377 );
2378 pipe(writable);
2379 });
2380 expect(getMeaningfulChildren(document)).toEqual(
2381 <html>
2382 <head />
2383 <body>
2384 <div>loading A...</div>
2385 </body>
2386 </html>,
2387 );
2388
2389 await act(() => {
2390 resolveText('unblock');
2391 resolveText('AAAA');
2392 resolveText('AA');
2393 });
2394 expect(getMeaningfulChildren(document)).toEqual(
2395 <html>
2396 <head />
2397 <body>
2398 <div>loading A...</div>
2399 <link rel="preload" as="style" href="A" />
2400 <link rel="preload" as="style" href="AA" />
2401 <link rel="preload" as="style" href="AAA" />
2402 <link rel="preload" as="style" href="AAAA" />
2403 </body>
2404 </html>,
2405 );
2406
2407 await act(() => {
2408 resolveText('A');
2409 });
2410 await act(() => {
2411 document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2412 const event = document.createEvent('Events');
2413 event.initEvent('load', true, true);
2414 l.dispatchEvent(event);
2415 });
2416 });
2417 expect(getMeaningfulChildren(document)).toEqual(
2418 <html>
2419 <head>
2420 <link rel="stylesheet" href="A" data-precedence="A" />
2421 <link rel="stylesheet" href="AA" data-precedence="AA" />
2422 </head>
2423 <body>
2424 <div>
2425 {'A'}
2426 {'AA'}
2427 {'loading AAA...'}
2428 </div>
2429 <link rel="preload" as="style" href="A" />
2430 <link rel="preload" as="style" href="AA" />
2431 <link rel="preload" as="style" href="AAA" />
2432 <link rel="preload" as="style" href="AAAA" />
2433 </body>
2434 </html>,
2435 );
2436
2437 await act(() => {
2438 resolveText('AAA');
2439 });
2440 await act(() => {
2441 document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2442 const event = document.createEvent('Events');
2443 event.initEvent('load', true, true);
2444 l.dispatchEvent(event);
2445 });
2446 });
2447 expect(getMeaningfulChildren(document)).toEqual(
2448 <html>
2449 <head>
2450 <link rel="stylesheet" href="A" data-precedence="A" />
2451 <link rel="stylesheet" href="AA" data-precedence="AA" />
2452 <link rel="stylesheet" href="AAA" data-precedence="AAA" />
2453 <link rel="stylesheet" href="AAAA" data-precedence="AAAA" />
2454 </head>
2455 <body>
2456 <div>
2457 {'A'}
2458 {'AA'}
2459 {'AAA'}
2460 {'AAAA'}
2461 </div>
2462 <link rel="preload" as="style" href="A" />
2463 <link rel="preload" as="style" href="AA" />
2464 <link rel="preload" as="style" href="AAA" />
2465 <link rel="preload" as="style" href="AAAA" />
2466 </body>
2467 </html>,
2468 );
2469 });
2470
2471 it('always enforces crossOrigin "anonymous" for font preloads', async () => {
2472 function App() {
2473 ReactDOM.preload('foo', {as: 'font', type: 'font/woff2'});
2474 ReactDOM.preload('bar', {as: 'font', crossOrigin: 'foo'});
2475 ReactDOM.preload('baz', {as: 'font', crossOrigin: 'use-credentials'});
2476 ReactDOM.preload('qux', {as: 'font', crossOrigin: 'anonymous'});
2477 return (
2478 <html>
2479 <head />
2480 <body />
2481 </html>
2482 );
2483 }
2484 await act(() => {
2485 const {pipe} = renderToPipeableStream(<App />);
2486 pipe(writable);
2487 });
2488 expect(getMeaningfulChildren(document)).toEqual(
2489 <html>
2490 <head>
2491 <link
2492 rel="preload"
2493 as="font"
2494 href="foo"
2495 crossorigin=""
2496 type="font/woff2"
2497 />
2498 <link rel="preload" as="font" href="bar" crossorigin="" />
2499 <link rel="preload" as="font" href="baz" crossorigin="" />
2500 <link rel="preload" as="font" href="qux" crossorigin="" />
2501 </head>
2502 <body />
2503 </html>,
2504 );
2505 });
2506
2507 it('does not hoist anything with an itemprop prop', async () => {
2508 function App() {
2509 return (
2510 <html>
2511 <head>
2512 <meta itemProp="outside" content="unscoped" />
2513 <link itemProp="link" rel="foo" href="foo" />
2514 <title itemProp="outside-title">title</title>
2515 <link
2516 itemProp="outside-stylesheet"
2517 rel="stylesheet"
2518 href="bar"
2519 precedence="default"
2520 />
2521 <style itemProp="outside-style" href="baz" precedence="default">
2522 outside style
2523 </style>
2524 <script itemProp="outside-script" async={true} src="qux" />
2525 </head>
2526 <body>
2527 <div itemScope={true}>
2528 <div>
2529 <meta itemProp="inside-meta" content="scoped" />
2530 <link itemProp="inside-link" rel="foo" href="foo" />
2531 <title itemProp="inside-title">title</title>
2532 <link
2533 itemProp="inside-stylesheet"
2534 rel="stylesheet"
2535 href="bar"
2536 precedence="default"
2537 />
2538 <style itemProp="inside-style" href="baz" precedence="default">
2539 inside style
2540 </style>
2541 <script itemProp="inside-script" async={true} src="qux" />
2542 </div>
2543 </div>
2544 </body>
2545 </html>
2546 );
2547 }
2548 await act(() => {
2549 renderToPipeableStream(<App />).pipe(writable);
2550 });
2551
2552 expect(getMeaningfulChildren(document)).toEqual(
2553 <html>
2554 <head>
2555 <meta itemprop="outside" content="unscoped" />
2556 <link itemprop="link" rel="foo" href="foo" />
2557 <title itemprop="outside-title">title</title>
2558 <link
2559 itemprop="outside-stylesheet"
2560 rel="stylesheet"
2561 href="bar"
2562 precedence="default"
2563 />
2564 <style itemprop="outside-style" href="baz" precedence="default">
2565 outside style
2566 </style>
2567 <script itemprop="outside-script" async="" src="qux" />
2568 </head>
2569 <body>
2570 <div itemscope="">
2571 <div>
2572 <meta itemprop="inside-meta" content="scoped" />
2573 <link itemprop="inside-link" rel="foo" href="foo" />
2574 <title itemprop="inside-title">title</title>
2575 <link
2576 itemprop="inside-stylesheet"
2577 rel="stylesheet"
2578 href="bar"
2579 precedence="default"
2580 />
2581 <style itemprop="inside-style" href="baz" precedence="default">
2582 inside style
2583 </style>
2584 <script itemprop="inside-script" async="" src="qux" />
2585 </div>
2586 </div>
2587 </body>
2588 </html>,
2589 );
2590
2591 ReactDOMClient.hydrateRoot(document, <App />);
2592 await waitForAll([]);
2593
2594 expect(getMeaningfulChildren(document)).toEqual(
2595 <html>
2596 <head>
2597 <meta itemprop="outside" content="unscoped" />
2598 <link itemprop="link" rel="foo" href="foo" />
2599 <title itemprop="outside-title">title</title>
2600 <link
2601 itemprop="outside-stylesheet"
2602 rel="stylesheet"
2603 href="bar"
2604 precedence="default"
2605 />
2606 <style itemprop="outside-style" href="baz" precedence="default">
2607 outside style
2608 </style>
2609 <script itemprop="outside-script" async="" src="qux" />
2610 </head>
2611 <body>
2612 <div itemscope="">
2613 <div>
2614 <meta itemprop="inside-meta" content="scoped" />
2615 <link itemprop="inside-link" rel="foo" href="foo" />
2616 <title itemprop="inside-title">title</title>
2617 <link
2618 itemprop="inside-stylesheet"
2619 rel="stylesheet"
2620 href="bar"
2621 precedence="default"
2622 />
2623 <style itemprop="inside-style" href="baz" precedence="default">
2624 inside style
2625 </style>
2626 <script itemprop="inside-script" async="" src="qux" />
2627 </div>
2628 </div>
2629 </body>
2630 </html>,
2631 );
2632 });
2633
2634 it('warns if you render <meta> tag with itemProp outside <body> or <head>', async () => {
2635 const root = ReactDOMClient.createRoot(document);
2636 root.render(
2637 <html>
2638 <meta itemProp="foo" />
2639 </html>,
2640 );
2641
2642 await waitForAll([]);
2643 assertConsoleErrorDev([
2644 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. ' +
2645 '`itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. ' +
2646 'If you were intending for React to hoist this <meta> remove the `itemProp` prop. ' +
2647 'Otherwise, try moving this tag into the <head> or <body> of the Document.\n' +
2648 ' in html (at **)',
2649 'In HTML, <meta> cannot be a child of <html>.\n' +
2650 'This will cause a hydration error.\n' +
2651 '\n' +
2652 '> <html>\n' +
2653 '> <meta itemProp="foo">' +
2654 '\n' +
2655 '\n in meta (at **)',
2656 ]);
2657 });
2658
2659 it('warns if you render a <title> tag with itemProp outside <body> or <head>', async () => {
2660 const root = ReactDOMClient.createRoot(document);
2661 root.render(
2662 <html>
2663 <title itemProp="foo">title</title>
2664 </html>,
2665 );
2666
2667 await waitForAll([]);
2668 assertConsoleErrorDev([
2669 'Cannot render a <title> outside the main document if it has an `itemProp` prop. ' +
2670 '`itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. ' +
2671 'If you were intending for React to hoist this <title> remove the `itemProp` prop. ' +
2672 'Otherwise, try moving this tag into the <head> or <body> of the Document.\n' +
2673 ' in html (at **)',
2674 'In HTML, <title> cannot be a child of <html>.\n' +
2675 'This will cause a hydration error.\n' +
2676 '\n' +
2677 '> <html>\n' +
2678 '> <title itemProp="foo">' +
2679 '\n' +
2680 '\n in title (at **)',
2681 ]);
2682 });
2683
2684 it('warns if you render a <style> tag with itemProp outside <body> or <head>', async () => {
2685 const root = ReactDOMClient.createRoot(document);
2686 root.render(
2687 <html>
2688 <style itemProp="foo">style</style>
2689 </html>,
2690 );
2691
2692 await waitForAll([]);
2693 assertConsoleErrorDev([
2694 'Cannot render a <style> outside the main document if it has an `itemProp` prop. ' +
2695 '`itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. ' +
2696 'If you were intending for React to hoist this <style> remove the `itemProp` prop. ' +
2697 'Otherwise, try moving this tag into the <head> or <body> of the Document.\n' +
2698 ' in html (at **)',
2699 'In HTML, <style> cannot be a child of <html>.\n' +
2700 'This will cause a hydration error.\n' +
2701 '\n' +
2702 '> <html>\n' +
2703 '> <style itemProp="foo">' +
2704 '\n' +
2705 '\n in style (at **)',
2706 ]);
2707 });
2708
2709 it('warns if you render a <link> tag with itemProp outside <body> or <head>', async () => {
2710 const root = ReactDOMClient.createRoot(document);
2711 root.render(
2712 <html>
2713 <link itemProp="foo" />
2714 </html>,
2715 );
2716
2717 await waitForAll([]);
2718 assertConsoleErrorDev([
2719 'Cannot render a <link> outside the main document if it has an `itemProp` prop. ' +
2720 '`itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. ' +
2721 'If you were intending for React to hoist this <link> remove the `itemProp` prop. ' +
2722 'Otherwise, try moving this tag into the <head> or <body> of the Document.\n' +
2723 ' in html (at **)',
2724 'In HTML, <link> cannot be a child of <html>.\n' +
2725 'This will cause a hydration error.\n' +
2726 '\n' +
2727 '> <html>\n' +
2728 '> <link itemProp="foo">\n' +
2729 '\n' +
2730 ' in link (at **)',
2731 ]);
2732 });
2733
2734 it('warns if you render a <script> tag with itemProp outside <body> or <head>', async () => {
2735 const root = ReactDOMClient.createRoot(document);
2736 root.render(
2737 <html>
2738 <script itemProp="foo" />
2739 </html>,
2740 );
2741
2742 await waitForAll([]);
2743 assertConsoleErrorDev([
2744 'Cannot render a <script> outside the main document if it has an `itemProp` prop. ' +
2745 '`itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. ' +
2746 'If you were intending for React to hoist this <script> remove the `itemProp` prop. ' +
2747 'Otherwise, try moving this tag into the <head> or <body> of the Document.\n' +
2748 ' in html (at **)',
2749 'In HTML, <script> cannot be a child of <html>.\n' +
2750 'This will cause a hydration error.\n' +
2751 '\n' +
2752 '> <html>\n' +
2753 '> <script itemProp="foo">\n' +
2754 '\n' +
2755 ' in script (at **)',
2756 ...(gate('enableTrustedTypesIntegration')
2757 ? [
2758 'Encountered a script tag while rendering React component. ' +
2759 'Scripts inside React components are never executed when rendering on the client. ' +
2760 'Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' +
2761 ' in script (at **)',
2762 ]
2763 : []),
2764 ]);
2765 });
2766
2767 it('can hydrate resources and components in the head and body even if a browser or 3rd party script injects extra html nodes', async () => {
2768 // This is a stress test case for hydrating a complex combination of hoistable elements, hoistable resources and host components
2769 // in an environment that has been manipulated by 3rd party scripts/extensions to modify the <head> and <body>
2770 function App() {
2771 return (
2772 <>
2773 <link rel="foo" href="foo" />
2774 <script async={true} src="rendered" />
2775 <link rel="stylesheet" href="stylesheet" precedence="default" />
2776 <html itemScope={true}>
2777 <head>
2778 {/* Component */}
2779 <link rel="stylesheet" href="stylesheet" />
2780 <script src="sync rendered" data-meaningful="" />
2781 <style>{'body { background-color: red; }'}</style>
2782 <script src="async rendered" async={true} onLoad={() => {}} />
2783 <noscript>
2784 <meta name="noscript" content="noscript" />
2785 </noscript>
2786 <link rel="foo" href="foo" onLoad={() => {}} />
2787 </head>
2788 <body>
2789 {/* Component because it has itemProp */}
2790 <meta name="foo" content="foo" itemProp="a prop" />
2791 {/* regular Hoistable */}
2792 <meta name="foo" content="foo" />
2793 {/* regular Hoistable */}
2794 <title>title</title>
2795 <div itemScope={true}>
2796 <div>
2797 <div>deep hello</div>
2798 {/* Component because it has itemProp */}
2799 <meta name="foo" content="foo" itemProp="a prop" />
2800 </div>
2801 </div>
2802 </body>
2803 </html>
2804 <link rel="foo" href="foo" />
2805 </>
2806 );
2807 }
2808
2809 await act(() => {
2810 renderToPipeableStream(<App />).pipe(writable);
2811 });
2812
2813 expect(getMeaningfulChildren(document)).toEqual(
2814 <html itemscope="">
2815 <head>
2816 {/* Hoisted Resources and elements */}
2817 <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2818 <script async="" src="rendered" />
2819 <link rel="foo" href="foo" />
2820 <meta name="foo" content="foo" />
2821 <title>title</title>
2822 <link rel="foo" href="foo" />
2823 {/* rendered host components */}
2824 <link rel="stylesheet" href="stylesheet" />
2825 <script src="sync rendered" data-meaningful="" />
2826 <style>{'body { background-color: red; }'}</style>
2827 <script src="async rendered" async="" />
2828 <noscript>&lt;meta name="noscript" content="noscript"&gt;</noscript>
2829 <link rel="foo" href="foo" />
2830 </head>
2831 <body>
2832 <meta name="foo" content="foo" itemprop="a prop" />
2833 <div itemscope="">
2834 <div>
2835 <div>deep hello</div>
2836 <meta name="foo" content="foo" itemprop="a prop" />
2837 </div>
2838 </div>
2839 </body>
2840 </html>,
2841 );
2842
2843 // We inject some styles, divs, scripts into the begginning, middle, and end
2844 // of the head / body.
2845 const injectedStyle = document.createElement('style');
2846 injectedStyle.textContent = 'body { background-color: blue; }';
2847 document.head.prepend(injectedStyle.cloneNode(true));
2848 document.head.appendChild(injectedStyle.cloneNode(true));
2849 document.body.prepend(injectedStyle.cloneNode(true));
2850 document.body.appendChild(injectedStyle.cloneNode(true));
2851
2852 const injectedDiv = document.createElement('div');
2853 document.head.prepend(injectedDiv);
2854 document.head.appendChild(injectedDiv.cloneNode(true));
2855 // We do not prepend a <div> in body because this will conflict with hyration
2856 // We still mostly hydrate by matchign tag and <div> does not have any attributes to
2857 // differentiate between likely-inject and likely-rendered cases. If a <div> is prepended
2858 // in the <body> and you render a <div> as the first child of <body> there will be a conflict.
2859 // We consider this a rare edge case and even if it does happen the fallback to client rendering
2860 // should patch up the DOM correctly
2861 document.body.appendChild(injectedDiv.cloneNode(true));
2862
2863 const injectedScript = document.createElement('script');
2864 injectedScript.setAttribute('async', '');
2865 injectedScript.setAttribute('src', 'injected');
2866 document.head.prepend(injectedScript);
2867 document.head.appendChild(injectedScript.cloneNode(true));
2868 document.body.prepend(injectedScript.cloneNode(true));
2869 document.body.appendChild(injectedScript.cloneNode(true));
2870
2871 // We hydrate the same App and confirm the output is identical except for the async
2872 // script insertion that happens because we do not SSR async scripts with load handlers.
2873 // All the extra inject nodes are preset
2874 const root = ReactDOMClient.hydrateRoot(document, <App />);
2875 await waitForAll([]);
2876 expect(getMeaningfulChildren(document)).toEqual(
2877 <html itemscope="">
2878 <head>
2879 <script async="" src="injected" />
2880 <div />
2881 <style>{'body { background-color: blue; }'}</style>
2882 <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2883 <script async="" src="rendered" />
2884 <link rel="foo" href="foo" />
2885 <meta name="foo" content="foo" />
2886 <title>title</title>
2887 <link rel="foo" href="foo" />
2888 <link rel="stylesheet" href="stylesheet" />
2889 <script src="sync rendered" data-meaningful="" />
2890 <style>{'body { background-color: red; }'}</style>
2891 <script src="async rendered" async="" />
2892 <noscript>&lt;meta name="noscript" content="noscript"&gt;</noscript>
2893 <link rel="foo" href="foo" />
2894 <style>{'body { background-color: blue; }'}</style>
2895 <div />
2896 <script async="" src="injected" />
2897 </head>
2898 <body>
2899 <script async="" src="injected" />
2900 <style>{'body { background-color: blue; }'}</style>
2901 <meta name="foo" content="foo" itemprop="a prop" />
2902 <div itemscope="">
2903 <div>
2904 <div>deep hello</div>
2905 <meta name="foo" content="foo" itemprop="a prop" />
2906 </div>
2907 </div>
2908 <style>{'body { background-color: blue; }'}</style>
2909 <div />
2910 <script async="" src="injected" />
2911 </body>
2912 </html>,
2913 );
2914
2915 // We unmount. The nodes that remain are
2916 // 1. Hoisted resources (we don't clean these up on unmount to address races with streaming suspense and navigation)
2917 // 2. preloads that are injected to hint the browser to load a resource but are not associated to Fibers directly
2918 // 3. Nodes that React skipped over during hydration
2919 root.unmount();
2920 expect(getMeaningfulChildren(document)).toEqual(
2921 <html>
2922 <head>
2923 <script async="" src="injected" />
2924 <div />
2925 <style>{'body { background-color: blue; }'}</style>
2926 <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2927 <script async="" src="rendered" />
2928 <style>{'body { background-color: blue; }'}</style>
2929 <div />
2930 <script async="" src="injected" />
2931 </head>
2932 <body>
2933 <script async="" src="injected" />
2934 <style>{'body { background-color: blue; }'}</style>
2935 <style>{'body { background-color: blue; }'}</style>
2936 <div />
2937 <script async="" src="injected" />
2938 </body>
2939 </html>,
2940 );
2941 });
2942
2943 it('does not preload nomodule scripts', async () => {
2944 await act(() => {
2945 renderToPipeableStream(
2946 <html>
2947 <body>
2948 <script src="foo" noModule={true} data-meaningful="" />
2949 <script async={true} src="bar" noModule={true} data-meaningful="" />
2950 </body>
2951 </html>,
2952 ).pipe(writable);
2953 });
2954 expect(getMeaningfulChildren(document)).toEqual(
2955 <html>
2956 <head>
2957 <script async="" src="bar" nomodule="" data-meaningful="" />
2958 </head>
2959 <body>
2960 <script src="foo" nomodule="" data-meaningful="" />
2961 </body>
2962 </html>,
2963 );
2964 });
2965
2966 it('can delay commit until css resources load', async () => {
2967 const root = ReactDOMClient.createRoot(container);
2968 expect(getMeaningfulChildren(container)).toBe(undefined);
2969 React.startTransition(() => {
2970 root.render(
2971 <>
2972 <link rel="stylesheet" href="foo" precedence="default" />
2973 <div>hello</div>
2974 </>,
2975 );
2976 });
2977 await waitForAll([]);
2978 expect(getMeaningfulChildren(container)).toBe(undefined);
2979 expect(getMeaningfulChildren(document.head)).toEqual(
2980 <link rel="preload" as="style" href="foo" />,
2981 );
2982
2983 loadPreloads();
2984 assertLog(['load preload: foo']);
2985
2986 // We expect that the stylesheet is inserted now but the commit has not happened yet.
2987 expect(getMeaningfulChildren(container)).toBe(undefined);
2988 expect(getMeaningfulChildren(document.head)).toEqual([
2989 <link rel="stylesheet" href="foo" data-precedence="default" />,
2990 <link rel="preload" as="style" href="foo" />,
2991 ]);
2992
2993 loadStylesheets();
2994 assertLog(['load stylesheet: foo']);
2995
2996 // We expect that the commit finishes synchronously after the stylesheet loads.
2997 expect(getMeaningfulChildren(container)).toEqual(<div>hello</div>);
2998 expect(getMeaningfulChildren(document.head)).toEqual([
2999 <link rel="stylesheet" href="foo" data-precedence="default" />,
3000 <link rel="preload" as="style" href="foo" />,
3001 ]);
3002 });
3003
3004 // https://github.com/facebook/react/issues/27585
3005 it('does not reinsert already inserted stylesheets during a delayed commit', async () => {
3006 await act(() => {
3007 renderToPipeableStream(
3008 <html>
3009 <body>
3010 <link rel="stylesheet" href="first" precedence="default" />
3011 <link rel="stylesheet" href="second" precedence="default" />
3012 server
3013 </body>
3014 </html>,
3015 ).pipe(writable);
3016 });
3017
3018 expect(getMeaningfulChildren(document)).toEqual(
3019 <html>
3020 <head>
3021 <link rel="stylesheet" href="first" data-precedence="default" />
3022 <link rel="stylesheet" href="second" data-precedence="default" />
3023 </head>
3024 <body>server</body>
3025 </html>,
3026 );
3027
3028 const root = ReactDOMClient.createRoot(document.body);
3029 expect(getMeaningfulChildren(container)).toBe(undefined);
3030 root.render(
3031 <>
3032 <link rel="stylesheet" href="first" precedence="default" />
3033 <link rel="stylesheet" href="third" precedence="default" />
3034 <div>client</div>
3035 </>,
3036 );
3037 await waitForAll([]);
3038 await act(() => {
3039 loadPreloads();
3040 loadStylesheets();
3041 });
3042 await assertLog([
3043 'load preload: third',
3044 'load stylesheet: first',
3045 'load stylesheet: second',
3046 'load stylesheet: third',
3047 ]);
3048 expect(getMeaningfulChildren(document)).toEqual(
3049 <html>
3050 <head>
3051 <link rel="stylesheet" href="first" data-precedence="default" />
3052 <link rel="stylesheet" href="second" data-precedence="default" />
3053 <link rel="stylesheet" href="third" data-precedence="default" />
3054 <link rel="preload" href="third" as="style" />
3055 </head>
3056 <body>
3057 <div>client</div>
3058 </body>
3059 </html>,
3060 );
3061
3062 // In a transition we add another reference to an already loaded resource
3063 // https://github.com/facebook/react/issues/27585
3064 React.startTransition(() => {
3065 root.render(
3066 <>
3067 <link rel="stylesheet" href="first" precedence="default" />
3068 <link rel="stylesheet" href="third" precedence="default" />
3069 <div>client</div>
3070 <link rel="stylesheet" href="first" precedence="default" />
3071 </>,
3072 );
3073 });
3074 await waitForAll([]);
3075 // In https://github.com/facebook/react/issues/27585 the order updated
3076 // to second, third, first
3077 expect(getMeaningfulChildren(document)).toEqual(
3078 <html>
3079 <head>
3080 <link rel="stylesheet" href="first" data-precedence="default" />
3081 <link rel="stylesheet" href="second" data-precedence="default" />
3082 <link rel="stylesheet" href="third" data-precedence="default" />
3083 <link rel="preload" href="third" as="style" />
3084 </head>
3085 <body>
3086 <div>client</div>
3087 </body>
3088 </html>,
3089 );
3090 });
3091
3092 // eslint-disable-next-line jest/no-disabled-tests
3093 it.skip('can delay commit until css resources error', async () => {
3094 // TODO: This test fails and crashes jest. need to figure out why before unskipping.
3095 const root = ReactDOMClient.createRoot(container);
3096 expect(getMeaningfulChildren(container)).toBe(undefined);
3097 React.startTransition(() => {
3098 root.render(
3099 <>
3100 <link rel="stylesheet" href="foo" precedence="default" />
3101 <link rel="stylesheet" href="bar" precedence="default" />
3102 <div>hello</div>
3103 </>,
3104 );
3105 });
3106 await waitForAll([]);
3107 expect(getMeaningfulChildren(container)).toBe(undefined);
3108 expect(getMeaningfulChildren(document.head)).toEqual([
3109 <link rel="preload" as="style" href="foo" />,
3110 <link rel="preload" as="style" href="bar" />,
3111 ]);
3112
3113 loadPreloads(['foo']);
3114 errorPreloads(['bar']);
3115 assertLog(['load preload: foo', 'error preload: bar']);
3116
3117 // We expect that the stylesheet is inserted now but the commit has not happened yet.
3118 expect(getMeaningfulChildren(container)).toBe(undefined);
3119 expect(getMeaningfulChildren(document.head)).toEqual([
3120 <link rel="stylesheet" href="foo" data-precedence="default" />,
3121 <link rel="stylesheet" href="bar" data-precedence="default" />,
3122 <link rel="preload" as="style" href="foo" />,
3123 <link rel="preload" as="style" href="bar" />,
3124 ]);
3125
3126 errorStylesheets(['bar']);
3127
3128 loadStylesheets(['foo']);
3129 assertLog(['load stylesheet: foo', 'error stylesheet: bar']);
3130
3131 // We expect that the commit finishes synchronously after the stylesheet loads.
3132 expect(getMeaningfulChildren(container)).toEqual(<div>hello</div>);
3133 expect(getMeaningfulChildren(document.head)).toEqual([
3134 <link rel="stylesheet" href="foo" data-precedence="default" />,
3135 <link rel="stylesheet" href="bar" data-precedence="default" />,
3136 <link rel="preload" as="style" href="foo" />,
3137 <link rel="preload" as="style" href="bar" />,
3138 ]);
3139 });
3140
3141 it('assumes stylesheets that load in the shell loaded already', async () => {
3142 await act(() => {
3143 renderToPipeableStream(
3144 <html>
3145 <body>
3146 <link rel="stylesheet" href="foo" precedence="default" />
3147 hello
3148 </body>
3149 </html>,
3150 ).pipe(writable);
3151 });
3152
3153 let root;
3154 React.startTransition(() => {
3155 root = ReactDOMClient.hydrateRoot(
3156 document,
3157 <html>
3158 <body>
3159 <link rel="stylesheet" href="foo" precedence="default" />
3160 hello
3161 </body>
3162 </html>,
3163 );
3164 });
3165 await waitForAll([]);
3166 expect(getMeaningfulChildren(document)).toEqual(
3167 <html>
3168 <head>
3169 <link rel="stylesheet" href="foo" data-precedence="default" />
3170 </head>
3171 <body>hello</body>
3172 </html>,
3173 );
3174
3175 React.startTransition(() => {
3176 root.render(
3177 <html>
3178 <body>
3179 <link rel="stylesheet" href="foo" precedence="default" />
3180 hello2
3181 </body>
3182 </html>,
3183 );
3184 });
3185 await waitForAll([]);
3186 expect(getMeaningfulChildren(document)).toEqual(
3187 <html>
3188 <head>
3189 <link rel="stylesheet" href="foo" data-precedence="default" />
3190 </head>
3191 <body>hello2</body>
3192 </html>,
3193 );
3194
3195 React.startTransition(() => {
3196 root.render(
3197 <html>
3198 <body>
3199 <link rel="stylesheet" href="foo" precedence="default" />
3200 hello3
3201 <link rel="stylesheet" href="bar" precedence="default" />
3202 </body>
3203 </html>,
3204 );
3205 });
3206 await waitForAll([]);
3207 expect(getMeaningfulChildren(document)).toEqual(
3208 <html>
3209 <head>
3210 <link rel="stylesheet" href="foo" data-precedence="default" />
3211 <link rel="preload" href="bar" as="style" />
3212 </head>
3213 <body>hello2</body>
3214 </html>,
3215 );
3216
3217 loadPreloads();
3218 assertLog(['load preload: bar']);
3219 expect(getMeaningfulChildren(document)).toEqual(
3220 <html>
3221 <head>
3222 <link rel="stylesheet" href="foo" data-precedence="default" />
3223 <link rel="stylesheet" href="bar" data-precedence="default" />
3224 <link rel="preload" href="bar" as="style" />
3225 </head>
3226 <body>hello2</body>
3227 </html>,
3228 );
3229
3230 loadStylesheets(['bar']);
3231 assertLog(['load stylesheet: bar']);
3232 expect(getMeaningfulChildren(document)).toEqual(
3233 <html>
3234 <head>
3235 <link rel="stylesheet" href="foo" data-precedence="default" />
3236 <link rel="stylesheet" href="bar" data-precedence="default" />
3237 <link rel="preload" href="bar" as="style" />
3238 </head>
3239 <body>hello3</body>
3240 </html>,
3241 );
3242 });
3243
3244 it('can interrupt a suspended commit with a new update', async () => {
3245 function App({children}) {
3246 return (
3247 <html>
3248 <body>{children}</body>
3249 </html>
3250 );
3251 }
3252 const root = ReactDOMClient.createRoot(document);
3253
3254 // Do an initial render. This means subsequent insertions will suspend,
3255 // unless they are wrapped inside a fresh Suspense boundary.
3256 root.render(<App />);
3257 await waitForAll([]);
3258
3259 // Insert a stylesheet. This will suspend because it's a transition.
3260 React.startTransition(() => {
3261 root.render(
3262 <App>
3263 hello
3264 <link rel="stylesheet" href="foo" precedence="default" />
3265 </App>,
3266 );
3267 });
3268 await waitForAll([]);
3269 // Although the commit suspended, a preload was inserted.
3270 expect(getMeaningfulChildren(document)).toEqual(
3271 <html>
3272 <head>
3273 <link rel="preload" href="foo" as="style" />
3274 </head>
3275 <body />
3276 </html>,
3277 );
3278
3279 // Before the stylesheet has loaded, do an urgent update. This will insert a
3280 // different stylesheet, and cancel the first one. This stylesheet will not
3281 // suspend, even though it hasn't loaded, because it's an urgent update.
3282 root.render(
3283 <App>
3284 hello2
3285 {null}
3286 <link rel="stylesheet" href="bar" precedence="default" />
3287 </App>,
3288 );
3289 await waitForAll([]);
3290 await act(() => {
3291 loadPreloads(['bar']);
3292 loadStylesheets(['bar']);
3293 });
3294 await assertLog(['load preload: bar', 'load stylesheet: bar']);
3295
3296 // The bar stylesheet was inserted. There's still a "foo" preload, even
3297 // though that update was superseded.
3298 expect(getMeaningfulChildren(document)).toEqual(
3299 <html>
3300 <head>
3301 <link rel="stylesheet" href="bar" data-precedence="default" />
3302 <link rel="preload" href="foo" as="style" />
3303 <link rel="preload" href="bar" as="style" />
3304 </head>
3305 <body>hello2</body>
3306 </html>,
3307 );
3308
3309 // When "foo" finishes loading, nothing happens, because "foo" was not
3310 // included in the last root update. However, if we insert "foo" again
3311 // later, it should immediately commit without suspending, because it's
3312 // been preloaded.
3313 loadPreloads(['foo']);
3314 assertLog(['load preload: foo']);
3315 expect(getMeaningfulChildren(document)).toEqual(
3316 <html>
3317 <head>
3318 <link rel="stylesheet" href="bar" data-precedence="default" />
3319 <link rel="preload" href="foo" as="style" />
3320 <link rel="preload" href="bar" as="style" />
3321 </head>
3322 <body>hello2</body>
3323 </html>,
3324 );
3325
3326 // Now insert "foo" again.
3327 React.startTransition(() => {
3328 root.render(
3329 <App>
3330 hello3
3331 <link rel="stylesheet" href="foo" precedence="default" />
3332 <link rel="stylesheet" href="bar" precedence="default" />
3333 </App>,
3334 );
3335 });
3336 await waitForAll([]);
3337 // Commits without suspending because "foo" was preloaded.
3338 expect(getMeaningfulChildren(document)).toEqual(
3339 <html>
3340 <head>
3341 <link rel="stylesheet" href="bar" data-precedence="default" />
3342 <link rel="stylesheet" href="foo" data-precedence="default" />
3343 <link rel="preload" href="foo" as="style" />
3344 <link rel="preload" href="bar" as="style" />
3345 </head>
3346 <body>hello3</body>
3347 </html>,
3348 );
3349
3350 loadStylesheets(['foo']);
3351 assertLog(['load stylesheet: foo']);
3352 expect(getMeaningfulChildren(document)).toEqual(
3353 <html>
3354 <head>
3355 <link rel="stylesheet" href="bar" data-precedence="default" />
3356 <link rel="stylesheet" href="foo" data-precedence="default" />
3357 <link rel="preload" href="foo" as="style" />
3358 <link rel="preload" href="bar" as="style" />
3359 </head>
3360 <body>hello3</body>
3361 </html>,
3362 );
3363 });
3364
3365 it('will put a Suspense boundary into fallback if it contains a stylesheet not loaded during a sync update', async () => {
3366 function App({children}) {
3367 return (
3368 <html>
3369 <body>{children}</body>
3370 </html>
3371 );
3372 }
3373 const root = ReactDOMClient.createRoot(document);
3374
3375 await clientAct(() => {
3376 root.render(<App />);
3377 });
3378 await waitForAll([]);
3379
3380 await clientAct(() => {
3381 root.render(
3382 <App>
3383 <Suspense fallback="loading...">
3384 <div>
3385 hello
3386 <link rel="stylesheet" href="foo" precedence="default" />
3387 </div>
3388 </Suspense>
3389 </App>,
3390 );
3391 });
3392 await waitForAll([]);
3393
3394 if (gate(flags => flags.alwaysThrottleRetries)) {
3395 // Although the commit suspended, a preload was inserted.
3396 expect(getMeaningfulChildren(document)).toEqual(
3397 <html>
3398 <head>
3399 <link rel="preload" href="foo" as="style" />
3400 </head>
3401 <body>loading...</body>
3402 </html>,
3403 );
3404
3405 loadPreloads(['foo']);
3406 assertLog(['load preload: foo']);
3407 expect(getMeaningfulChildren(document)).toEqual(
3408 <html>
3409 <head>
3410 <link rel="stylesheet" href="foo" data-precedence="default" />
3411 <link rel="preload" href="foo" as="style" />
3412 </head>
3413 <body>loading...</body>
3414 </html>,
3415 );
3416 }
3417
3418 loadStylesheets(['foo']);
3419 assertLog(['load stylesheet: foo']);
3420 expect(getMeaningfulChildren(document)).toEqual(
3421 <html>
3422 <head>
3423 <link rel="stylesheet" href="foo" data-precedence="default" />
3424 <link rel="preload" href="foo" as="style" />
3425 </head>
3426 <body>
3427 <div>hello</div>
3428 </body>
3429 </html>,
3430 );
3431
3432 await clientAct(() => {
3433 root.render(
3434 <App>
3435 <Suspense fallback="loading...">
3436 <div>
3437 hello
3438 <link rel="stylesheet" href="foo" precedence="default" />
3439 <link rel="stylesheet" href="bar" precedence="default" />
3440 </div>
3441 </Suspense>
3442 </App>,
3443 );
3444 });
3445 await waitForAll([]);
3446 if (gate(flags => flags.alwaysThrottleRetries)) {
3447 expect(getMeaningfulChildren(document)).toEqual(
3448 <html>
3449 <head>
3450 <link rel="stylesheet" href="foo" data-precedence="default" />
3451 <link rel="preload" href="foo" as="style" />
3452 <link rel="preload" href="bar" as="style" />
3453 </head>
3454 <body>
3455 <div style="display: none;">hello</div>loading...
3456 </body>
3457 </html>,
3458 );
3459
3460 loadPreloads(['bar']);
3461 assertLog(['load preload: bar']);
3462 expect(getMeaningfulChildren(document)).toEqual(
3463 <html>
3464 <head>
3465 <link rel="stylesheet" href="foo" data-precedence="default" />
3466 <link rel="stylesheet" href="bar" data-precedence="default" />
3467 <link rel="preload" href="foo" as="style" />
3468 <link rel="preload" href="bar" as="style" />
3469 </head>
3470 <body>
3471 <div style="display: none;">hello</div>loading...
3472 </body>
3473 </html>,
3474 );
3475 }
3476 loadStylesheets(['bar']);
3477 assertLog(['load stylesheet: bar']);
3478 expect(getMeaningfulChildren(document)).toEqual(
3479 <html>
3480 <head>
3481 <link rel="stylesheet" href="foo" data-precedence="default" />
3482 <link rel="stylesheet" href="bar" data-precedence="default" />
3483 <link rel="preload" href="foo" as="style" />
3484 <link rel="preload" href="bar" as="style" />
3485 </head>
3486 <body>
3487 <div style="">hello</div>
3488 </body>
3489 </html>,
3490 );
3491 });
3492
3493 it('will assume stylesheets already in the document have loaded if it cannot confirm it is not yet loaded', async () => {
3494 await act(() => {
3495 renderToPipeableStream(
3496 <html>
3497 <head>
3498 <link rel="stylesheet" href="foo" data-precedence="default" />
3499 </head>
3500 <body>
3501 <div id="foo" />
3502 </body>
3503 </html>,
3504 ).pipe(writable);
3505 });
3506
3507 const root = ReactDOMClient.createRoot(document.querySelector('#foo'));
3508
3509 root.render(
3510 <div>
3511 <Suspense fallback="loading...">
3512 <link rel="stylesheet" href="foo" precedence="default" />
3513 hello world
3514 </Suspense>
3515 </div>,
3516 );
3517
3518 await waitForAll([]);
3519 expect(getMeaningfulChildren(document)).toEqual(
3520 <html>
3521 <head>
3522 <link rel="stylesheet" href="foo" data-precedence="default" />
3523 </head>
3524 <body>
3525 <div id="foo">
3526 <div>hello world</div>
3527 </div>
3528 </body>
3529 </html>,
3530 );
3531 });
3532
3533 it('will assume wait for loading stylesheets to load before continuing', async () => {
3534 let ssr = true;
3535 function Component() {
3536 if (ssr) {
3537 return null;
3538 } else {
3539 return (
3540 <>
3541 <link rel="stylesheet" href="foo" precedence="default" />
3542 <div>hello client</div>
3543 </>
3544 );
3545 }
3546 }
3547
3548 await act(() => {
3549 renderToPipeableStream(
3550 <html>
3551 <body>
3552 <div>
3553 <Suspense fallback="loading...">
3554 <BlockedOn value="reveal">
3555 <link rel="stylesheet" href="foo" precedence="default" />
3556 <div>hello world</div>
3557 </BlockedOn>
3558 </Suspense>
3559 </div>
3560 <div>
3561 <Suspense fallback="loading 2...">
3562 <Component />
3563 </Suspense>
3564 </div>
3565 </body>
3566 </html>,
3567 ).pipe(writable);
3568 });
3569
3570 expect(getMeaningfulChildren(document)).toEqual(
3571 <html>
3572 <head />
3573 <body>
3574 <div>loading...</div>
3575 <div />
3576 </body>
3577 </html>,
3578 );
3579
3580 await act(() => {
3581 resolveText('reveal');
3582 });
3583
3584 expect(getMeaningfulChildren(document)).toEqual(
3585 <html>
3586 <head>
3587 <link rel="stylesheet" href="foo" data-precedence="default" />
3588 </head>
3589 <body>
3590 <div>loading...</div>
3591 <div />
3592 <link rel="preload" href="foo" as="style" />
3593 </body>
3594 </html>,
3595 );
3596
3597 ssr = false;
3598
3599 ReactDOMClient.hydrateRoot(
3600 document,
3601 <html>
3602 <body>
3603 <div>
3604 <Suspense fallback="loading...">
3605 <BlockedOn value="reveal">
3606 <link rel="stylesheet" href="foo" precedence="default" />
3607 <div>hello world</div>
3608 </BlockedOn>
3609 </Suspense>
3610 </div>
3611 <div>
3612 <Suspense fallback="loading 2...">
3613 <Component />
3614 </Suspense>
3615 </div>
3616 </body>
3617 </html>,
3618 );
3619 await waitForAll([]);
3620
3621 expect(getMeaningfulChildren(document)).toEqual(
3622 <html>
3623 <head>
3624 <link rel="stylesheet" href="foo" data-precedence="default" />
3625 </head>
3626 <body>
3627 <div>loading...</div>
3628 <div />
3629 <link rel="preload" href="foo" as="style" />
3630 </body>
3631 </html>,
3632 );
3633
3634 loadStylesheets();
3635 assertLog(['load stylesheet: foo']);
3636 await waitForAll([]);
3637 assertConsoleErrorDev([
3638 "Error: Hydration failed because the server rendered HTML didn't match the client. " +
3639 'As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n' +
3640 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
3641 "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
3642 "- Date formatting in a user's locale which doesn't match the server.\n" +
3643 '- External changing data without sending a snapshot of it along with the HTML.\n' +
3644 '- Invalid HTML tag nesting.\n\n' +
3645 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
3646 'https://react.dev/link/hydration-mismatch\n\n' +
3647 ' <html>\n' +
3648 ' <body>\n' +
3649 ' <div>\n' +
3650 ' <div>\n' +
3651 ' <Suspense fallback="loading 2...">\n' +
3652 ' <Component>\n' +
3653 ' <link>\n' +
3654 '+ <div>' +
3655 '\n in <stack>',
3656 ]);
3657 jest.runAllTimers();
3658
3659 expect(getMeaningfulChildren(document)).toEqual(
3660 <html>
3661 <head>
3662 <link rel="stylesheet" href="foo" data-precedence="default" />
3663 </head>
3664 <body>
3665 <div>
3666 <div>hello world</div>
3667 </div>
3668 <div>
3669 <div>hello client</div>
3670 </div>
3671 <link rel="preload" href="foo" as="style" />
3672 </body>
3673 </html>,
3674 );
3675 });
3676
3677 it('does not suspend a transition on a stylesheet whose preload has already loaded', async () => {
3678 const root = ReactDOMClient.createRoot(document);
3679 root.render(
3680 <html>
3681 <body>
3682 <Suspense fallback="loading...">initial</Suspense>
3683 </body>
3684 </html>,
3685 );
3686 await waitForAll([]);
3687
3688 ReactDOM.preload('route.css', {as: 'style'});
3689 expect(getMeaningfulChildren(document.head)).toEqual(
3690 <link rel="preload" href="route.css" as="style" />,
3691 );
3692 expect(getMeaningfulChildren(document.body)).toEqual('initial');
3693
3694 loadPreloads(['route.css']);
3695 assertLog(['load preload: route.css']);
3696
3697 React.startTransition(() => {
3698 root.render(
3699 <html>
3700 <body>
3701 <Suspense fallback="loading...">
3702 <link rel="stylesheet" href="route.css" precedence="default" />
3703 next
3704 </Suspense>
3705 </body>
3706 </html>,
3707 );
3708 });
3709 await waitForAll([]);
3710
3711 expect(getMeaningfulChildren(document.head)).toEqual([
3712 <link rel="stylesheet" href="route.css" data-precedence="default" />,
3713 <link rel="preload" href="route.css" as="style" />,
3714 ]);
3715 expect(getMeaningfulChildren(document.body)).toEqual('next');
3716
3717 loadStylesheets(['route.css']);
3718 assertLog(['load stylesheet: route.css']);
3719 expect(getMeaningfulChildren(document.head)).toEqual([
3720 <link rel="stylesheet" href="route.css" data-precedence="default" />,
3721 <link rel="preload" href="route.css" as="style" />,
3722 ]);
3723 expect(getMeaningfulChildren(document.body)).toEqual('next');
3724 });
3725
3726 it('suspends a transition on a stylesheet whose preload has not loaded yet', async () => {
3727 const root = ReactDOMClient.createRoot(document);
3728 root.render(
3729 <html>
3730 <body>
3731 <Suspense fallback="loading...">initial</Suspense>
3732 </body>
3733 </html>,
3734 );
3735 await waitForAll([]);
3736
3737 ReactDOM.preload('route.css', {as: 'style'});
3738 expect(getMeaningfulChildren(document.head)).toEqual(
3739 <link rel="preload" href="route.css" as="style" />,
3740 );
3741 expect(getMeaningfulChildren(document.body)).toEqual('initial');
3742
3743 React.startTransition(() => {
3744 root.render(
3745 <html>
3746 <body>
3747 <Suspense fallback="loading...">
3748 <link rel="stylesheet" href="route.css" precedence="default" />
3749 next
3750 </Suspense>
3751 </body>
3752 </html>,
3753 );
3754 });
3755 await waitForAll([]);
3756
3757 expect(getMeaningfulChildren(document.head)).toEqual(
3758 <link rel="preload" href="route.css" as="style" />,
3759 );
3760 expect(getMeaningfulChildren(document.body)).toEqual('initial');
3761
3762 loadPreloads(['route.css']);
3763 assertLog(['load preload: route.css']);
3764 await waitForAll([]);
3765 expect(getMeaningfulChildren(document.head)).toEqual([
3766 <link rel="stylesheet" href="route.css" data-precedence="default" />,
3767 <link rel="preload" href="route.css" as="style" />,
3768 ]);
3769 expect(getMeaningfulChildren(document.body)).toEqual('initial');
3770
3771 loadStylesheets(['route.css']);
3772 assertLog(['load stylesheet: route.css']);
3773 await waitForAll([]);
3774 expect(getMeaningfulChildren(document.head)).toEqual([
3775 <link rel="stylesheet" href="route.css" data-precedence="default" />,
3776 <link rel="preload" href="route.css" as="style" />,
3777 ]);
3778 expect(getMeaningfulChildren(document.body)).toEqual('next');
3779 });
3780
3781 it('can suspend commits on more than one root for the same resource at the same time', async () => {
3782 document.body.innerHTML = '';
3783 const container1 = document.createElement('div');
3784 const container2 = document.createElement('div');
3785 document.body.appendChild(container1);
3786 document.body.appendChild(container2);
3787
3788 const root1 = ReactDOMClient.createRoot(container1);
3789 const root2 = ReactDOMClient.createRoot(container2);
3790
3791 React.startTransition(() => {
3792 root1.render(
3793 <div>
3794 one
3795 <link rel="stylesheet" href="foo" precedence="default" />
3796 <link rel="stylesheet" href="one" precedence="default" />
3797 </div>,
3798 );
3799 });
3800 await waitForAll([]);
3801 React.startTransition(() => {
3802 root2.render(
3803 <div>
3804 two
3805 <link rel="stylesheet" href="foo" precedence="default" />
3806 <link rel="stylesheet" href="two" precedence="default" />
3807 </div>,
3808 );
3809 });
3810 await waitForAll([]);
3811
3812 expect(getMeaningfulChildren(document)).toEqual(
3813 <html>
3814 <head>
3815 <link rel="preload" href="foo" as="style" />
3816 <link rel="preload" href="one" as="style" />
3817 <link rel="preload" href="two" as="style" />
3818 </head>
3819 <body>
3820 <div />
3821 <div />
3822 </body>
3823 </html>,
3824 );
3825
3826 loadPreloads(['foo', 'two']);
3827 assertLog(['load preload: foo', 'load preload: two']);
3828 expect(getMeaningfulChildren(document)).toEqual(
3829 <html>
3830 <head>
3831 <link rel="stylesheet" href="foo" data-precedence="default" />
3832 <link rel="stylesheet" href="two" data-precedence="default" />
3833 <link rel="preload" href="foo" as="style" />
3834 <link rel="preload" href="one" as="style" />
3835 <link rel="preload" href="two" as="style" />
3836 </head>
3837 <body>
3838 <div />
3839 <div />
3840 </body>
3841 </html>,
3842 );
3843
3844 loadStylesheets(['foo', 'two']);
3845 assertLog(['load stylesheet: foo', 'load stylesheet: two']);
3846 expect(getMeaningfulChildren(document)).toEqual(
3847 <html>
3848 <head>
3849 <link rel="stylesheet" href="foo" data-precedence="default" />
3850 <link rel="stylesheet" href="two" data-precedence="default" />
3851 <link rel="preload" href="foo" as="style" />
3852 <link rel="preload" href="one" as="style" />
3853 <link rel="preload" href="two" as="style" />
3854 </head>
3855 <body>
3856 <div />
3857 <div>
3858 <div>two</div>
3859 </div>
3860 </body>
3861 </html>,
3862 );
3863
3864 loadPreloads();
3865 loadStylesheets();
3866 assertLog(['load preload: one', 'load stylesheet: one']);
3867 expect(getMeaningfulChildren(document)).toEqual(
3868 <html>
3869 <head>
3870 <link rel="stylesheet" href="foo" data-precedence="default" />
3871 <link rel="stylesheet" href="two" data-precedence="default" />
3872 <link rel="stylesheet" href="one" data-precedence="default" />
3873 <link rel="preload" href="foo" as="style" />
3874 <link rel="preload" href="one" as="style" />
3875 <link rel="preload" href="two" as="style" />
3876 </head>
3877 <body>
3878 <div>
3879 <div>one</div>
3880 </div>
3881 <div>
3882 <div>two</div>
3883 </div>
3884 </body>
3885 </html>,
3886 );
3887 });
3888
3889 it('stylesheets block render, with a really long timeout', async () => {
3890 function App({children}) {
3891 return (
3892 <html>
3893 <body>{children}</body>
3894 </html>
3895 );
3896 }
3897 const root = ReactDOMClient.createRoot(document);
3898 root.render(<App />);
3899 React.startTransition(() => {
3900 root.render(
3901 <App>
3902 hello
3903 <link rel="stylesheet" href="foo" precedence="default" />
3904 </App>,
3905 );
3906 });
3907 await waitForAll([]);
3908 expect(getMeaningfulChildren(document)).toEqual(
3909 <html>
3910 <head>
3911 <link rel="preload" href="foo" as="style" />
3912 </head>
3913 <body />
3914 </html>,
3915 );
3916
3917 // Advance time by 50 seconds. Even still, the transition is suspended.
3918 jest.advanceTimersByTime(50000);
3919 await waitForAll([]);
3920 expect(getMeaningfulChildren(document)).toEqual(
3921 <html>
3922 <head>
3923 <link rel="preload" href="foo" as="style" />
3924 </head>
3925 <body />
3926 </html>,
3927 );
3928
3929 // Advance time by 10 seconds more. A full minute total has elapsed. At this
3930 // point, something must have really gone wrong, so we time out and allow
3931 // unstyled content to be displayed.
3932 jest.advanceTimersByTime(10000);
3933 expect(getMeaningfulChildren(document)).toEqual(
3934 <html>
3935 <head>
3936 <link rel="stylesheet" href="foo" data-precedence="default" />
3937 <link rel="preload" href="foo" as="style" />
3938 </head>
3939 <body>hello</body>
3940 </html>,
3941 );
3942
3943 // We will load these after the commit finishes to ensure nothing errors and nothing new inserts
3944 loadPreloads(['foo']);
3945 loadStylesheets(['foo']);
3946 expect(getMeaningfulChildren(document)).toEqual(
3947 <html>
3948 <head>
3949 <link rel="stylesheet" href="foo" data-precedence="default" />
3950 <link rel="preload" href="foo" as="style" />
3951 </head>
3952 <body>hello</body>
3953 </html>,
3954 );
3955 });
3956
3957 it('can interrupt a suspended commit with a new transition', async () => {
3958 function App({children}) {
3959 return (
3960 <html>
3961 <body>{children}</body>
3962 </html>
3963 );
3964 }
3965 const root = ReactDOMClient.createRoot(document);
3966 root.render(<App>(empty)</App>);
3967
3968 // Start a transition to "A"
3969 React.startTransition(() => {
3970 root.render(
3971 <App>
3972 A
3973 <link rel="stylesheet" href="A" precedence="default" />
3974 </App>,
3975 );
3976 });
3977 await waitForAll([]);
3978
3979 // "A" hasn't loaded yet, so we remain on the initial UI. Its preload
3980 // has been inserted into the head, though.
3981 expect(getMeaningfulChildren(document)).toEqual(
3982 <html>
3983 <head>
3984 <link rel="preload" href="A" as="style" />
3985 </head>
3986 <body>(empty)</body>
3987 </html>,
3988 );
3989
3990 // Interrupt the "A" transition with a new one, "B"
3991 React.startTransition(() => {
3992 root.render(
3993 <App>
3994 B
3995 <link rel="stylesheet" href="B" precedence="default" />
3996 </App>,
3997 );
3998 });
3999 await waitForAll([]);
4000
4001 // Still on the initial UI because "B" hasn't loaded, but its preload
4002 // is now in the head, too.
4003 expect(getMeaningfulChildren(document)).toEqual(
4004 <html>
4005 <head>
4006 <link rel="preload" href="A" as="style" />
4007 <link rel="preload" href="B" as="style" />
4008 </head>
4009 <body>(empty)</body>
4010 </html>,
4011 );
4012
4013 // Finish loading
4014 loadPreloads();
4015 loadStylesheets();
4016 assertLog(['load preload: A', 'load preload: B', 'load stylesheet: B']);
4017 // The "B" transition has finished.
4018 expect(getMeaningfulChildren(document)).toEqual(
4019 <html>
4020 <head>
4021 <link rel="stylesheet" href="B" data-precedence="default" />
4022 <link rel="preload" href="A" as="style" />
4023 <link rel="preload" href="B" as="style" />
4024 </head>
4025 <body>B</body>
4026 </html>,
4027 );
4028 });
4029
4030 it('loading a stylesheet as part of an error boundary UI, during initial render', async () => {
4031 class ErrorBoundary extends React.Component {
4032 state = {error: null};
4033 static getDerivedStateFromError(error) {
4034 return {error};
4035 }
4036 render() {
4037 const error = this.state.error;
4038 if (error !== null) {
4039 return (
4040 <>
4041 <link rel="stylesheet" href="A" precedence="default" />
4042 {error.message}
4043 </>
4044 );
4045 }
4046 return this.props.children;
4047 }
4048 }
4049
4050 function Throws() {
4051 throw new Error('Oops!');
4052 }
4053
4054 function App() {
4055 return (
4056 <html>
4057 <body>
4058 <ErrorBoundary>
4059 <Suspense fallback="Loading...">
4060 <Throws />
4061 </Suspense>
4062 </ErrorBoundary>
4063 </body>
4064 </html>
4065 );
4066 }
4067
4068 // Initial server render. Because something threw, a Suspense fallback
4069 // is shown.
4070 await act(() => {
4071 renderToPipeableStream(<App />, {
4072 onError(x) {
4073 Scheduler.log('Caught server error: ' + x.message);
4074 },
4075 }).pipe(writable);
4076 });
4077 expect(getMeaningfulChildren(document)).toEqual(
4078 <html>
4079 <head />
4080 <body>Loading...</body>
4081 </html>,
4082 );
4083 assertLog(['Caught server error: Oops!']);
4084
4085 // Hydrate the tree. The error boundary will capture the error and attempt
4086 // to show an error screen. However, the error screen includes a stylesheet,
4087 // so the commit should suspend until the stylesheet loads.
4088 ReactDOMClient.hydrateRoot(document, <App />);
4089 await waitForAll([]);
4090
4091 // A preload for the stylesheet is inserted, but we still haven't committed
4092 // the error screen.
4093 expect(getMeaningfulChildren(document)).toEqual(
4094 <html>
4095 <head>
4096 <link as="style" href="A" rel="preload" />
4097 </head>
4098 <body>Loading...</body>
4099 </html>,
4100 );
4101
4102 // Finish loading the stylesheets. The commit should be unblocked, and the
4103 // error screen should appear.
4104 await clientAct(() => loadStylesheets());
4105 expect(getMeaningfulChildren(document)).toEqual(
4106 <html>
4107 <head>
4108 <link data-precedence="default" href="A" rel="stylesheet" />
4109 <link as="style" href="A" rel="preload" />
4110 </head>
4111 <body>Oops!</body>
4112 </html>,
4113 );
4114 });
4115
4116 it('will not flush a preload for a new rendered Stylesheet Resource if one was already flushed', async () => {
4117 function Component() {
4118 ReactDOM.preload('foo', {as: 'style'});
4119 return (
4120 <div>
4121 <Suspense fallback="loading...">
4122 <BlockedOn value="blocked">
4123 <link rel="stylesheet" href="foo" precedence="default" />
4124 hello
4125 </BlockedOn>
4126 </Suspense>
4127 </div>
4128 );
4129 }
4130 await act(() => {
4131 renderToPipeableStream(
4132 <html>
4133 <body>
4134 <Component />
4135 </body>
4136 </html>,
4137 ).pipe(writable);
4138 });
4139
4140 expect(getMeaningfulChildren(document)).toEqual(
4141 <html>
4142 <head>
4143 <link rel="preload" as="style" href="foo" />
4144 </head>
4145 <body>
4146 <div>loading...</div>
4147 </body>
4148 </html>,
4149 );
4150 await act(() => {
4151 resolveText('blocked');
4152 });
4153 await act(loadStylesheets);
4154 assertLog(['load stylesheet: foo']);
4155 expect(getMeaningfulChildren(document)).toEqual(
4156 <html>
4157 <head>
4158 <link rel="stylesheet" href="foo" data-precedence="default" />
4159 <link rel="preload" as="style" href="foo" />
4160 </head>
4161 <body>
4162 <div>hello</div>
4163 </body>
4164 </html>,
4165 );
4166 });
4167
4168 it('will not flush a preload for a new preinitialized Stylesheet Resource if one was already flushed', async () => {
4169 function Component() {
4170 ReactDOM.preload('foo', {as: 'style'});
4171 return (
4172 <div>
4173 <Suspense fallback="loading...">
4174 <BlockedOn value="blocked">
4175 <Preinit />
4176 hello
4177 </BlockedOn>
4178 </Suspense>
4179 </div>
4180 );
4181 }
4182
4183 function Preinit() {
4184 ReactDOM.preinit('foo', {as: 'style'});
4185 }
4186 await act(() => {
4187 renderToPipeableStream(
4188 <html>
4189 <body>
4190 <Component />
4191 </body>
4192 </html>,
4193 ).pipe(writable);
4194 });
4195
4196 expect(getMeaningfulChildren(document)).toEqual(
4197 <html>
4198 <head>
4199 <link rel="preload" as="style" href="foo" />
4200 </head>
4201 <body>
4202 <div>loading...</div>
4203 </body>
4204 </html>,
4205 );
4206 await act(() => {
4207 resolveText('blocked');
4208 });
4209 expect(getMeaningfulChildren(document)).toEqual(
4210 <html>
4211 <head>
4212 <link rel="preload" as="style" href="foo" />
4213 </head>
4214 <body>
4215 <div>hello</div>
4216 </body>
4217 </html>,
4218 );
4219 });
4220
4221 it('will not insert a preload if the underlying resource already exists in the Document', async () => {
4222 await act(() => {
4223 renderToPipeableStream(
4224 <html>
4225 <head>
4226 <link rel="stylesheet" href="foo" precedence="default" />
4227 <script async={true} src="bar" />
4228 <link rel="preload" href="baz" as="font" />
4229 </head>
4230 <body>
4231 <div id="container" />
4232 </body>
4233 </html>,
4234 ).pipe(writable);
4235 });
4236
4237 expect(getMeaningfulChildren(document)).toEqual(
4238 <html>
4239 <head>
4240 <link rel="stylesheet" href="foo" data-precedence="default" />
4241 <script async="" src="bar" />
4242 <link rel="preload" href="baz" as="font" />
4243 </head>
4244 <body>
4245 <div id="container" />
4246 </body>
4247 </html>,
4248 );
4249
4250 container = document.getElementById('container');
4251
4252 function ClientApp() {
4253 ReactDOM.preload('foo', {as: 'style'});
4254 ReactDOM.preload('bar', {as: 'script'});
4255 ReactDOM.preload('baz', {as: 'font'});
4256 return 'foo';
4257 }
4258
4259 const root = ReactDOMClient.createRoot(container);
4260
4261 await clientAct(() => root.render(<ClientApp />));
4262 expect(getMeaningfulChildren(document)).toEqual(
4263 <html>
4264 <head>
4265 <link rel="stylesheet" href="foo" data-precedence="default" />
4266 <script async="" src="bar" />
4267 <link rel="preload" href="baz" as="font" />
4268 </head>
4269 <body>
4270 <div id="container">foo</div>
4271 </body>
4272 </html>,
4273 );
4274 });
4275
4276 it('uses imageSrcSet and imageSizes when keying image preloads', async () => {
4277 function App({isClient}) {
4278 // Will key off href in absense of imageSrcSet
4279 ReactDOM.preload('foo', {as: 'image'});
4280 ReactDOM.preload('foo', {as: 'image'});
4281
4282 // Will key off imageSrcSet + imageSizes
4283 ReactDOM.preload('foo', {as: 'image', imageSrcSet: 'fooset'});
4284 ReactDOM.preload('foo2', {as: 'image', imageSrcSet: 'fooset'});
4285
4286 // Will key off imageSrcSet + imageSizes
4287 ReactDOM.preload('foo', {
4288 as: 'image',
4289 imageSrcSet: 'fooset',
4290 imageSizes: 'foosizes',
4291 });
4292 ReactDOM.preload('foo2', {
4293 as: 'image',
4294 imageSrcSet: 'fooset',
4295 imageSizes: 'foosizes',
4296 });
4297
4298 // Will key off href in absense of imageSrcSet, imageSizes is ignored. these should match the
4299 // first preloads not not emit a new preload tag
4300 ReactDOM.preload('foo', {as: 'image', imageSizes: 'foosizes'});
4301 ReactDOM.preload('foo', {as: 'image', imageSizes: 'foosizes'});
4302
4303 // These preloads are for something that isn't an image
4304 // They should all key off the href
4305 ReactDOM.preload('bar', {as: 'somethingelse'});
4306 ReactDOM.preload('bar', {
4307 as: 'somethingelse',
4308 imageSrcSet: 'makes no sense',
4309 });
4310 ReactDOM.preload('bar', {
4311 as: 'somethingelse',
4312 imageSrcSet: 'makes no sense',
4313 imageSizes: 'makes no sense',
4314 });
4315
4316 if (isClient) {
4317 // Will key off href in absense of imageSrcSet
4318 ReactDOM.preload('client', {as: 'image'});
4319 ReactDOM.preload('client', {as: 'image'});
4320
4321 // Will key off imageSrcSet + imageSizes
4322 ReactDOM.preload('client', {as: 'image', imageSrcSet: 'clientset'});
4323 ReactDOM.preload('client2', {as: 'image', imageSrcSet: 'clientset'});
4324
4325 // Will key off imageSrcSet + imageSizes
4326 ReactDOM.preload('client', {
4327 as: 'image',
4328 imageSrcSet: 'clientset',
4329 imageSizes: 'clientsizes',
4330 });
4331 ReactDOM.preload('client2', {
4332 as: 'image',
4333 imageSrcSet: 'clientset',
4334 imageSizes: 'clientsizes',
4335 });
4336
4337 // Will key off href in absense of imageSrcSet, imageSizes is ignored. these should match the
4338 // first preloads not not emit a new preload tag
4339 ReactDOM.preload('client', {as: 'image', imageSizes: 'clientsizes'});
4340 ReactDOM.preload('client', {as: 'image', imageSizes: 'clientsizes'});
4341 }
4342
4343 return (
4344 <html>
4345 <body>hello</body>
4346 </html>
4347 );
4348 }
4349
4350 await act(() => {
4351 renderToPipeableStream(<App />).pipe(writable);
4352 });
4353 expect(getMeaningfulChildren(document)).toEqual(
4354 <html>
4355 <head>
4356 <link rel="preload" as="image" href="foo" />
4357 <link rel="preload" as="image" imagesrcset="fooset" />
4358 <link
4359 rel="preload"
4360 as="image"
4361 imagesrcset="fooset"
4362 imagesizes="foosizes"
4363 />
4364 <link rel="preload" as="somethingelse" href="bar" />
4365 </head>
4366 <body>hello</body>
4367 </html>,
4368 );
4369
4370 const root = ReactDOMClient.hydrateRoot(document, <App />);
4371 await waitForAll([]);
4372 expect(getMeaningfulChildren(document)).toEqual(
4373 <html>
4374 <head>
4375 <link rel="preload" as="image" href="foo" />
4376 <link rel="preload" as="image" imagesrcset="fooset" />
4377 <link
4378 rel="preload"
4379 as="image"
4380 imagesrcset="fooset"
4381 imagesizes="foosizes"
4382 />
4383 <link rel="preload" as="somethingelse" href="bar" />
4384 </head>
4385 <body>hello</body>
4386 </html>,
4387 );
4388
4389 root.render(<App isClient={true} />);
4390 await waitForAll([]);
4391 expect(getMeaningfulChildren(document)).toEqual(
4392 <html>
4393 <head>
4394 <link rel="preload" as="image" href="foo" />
4395 <link rel="preload" as="image" imagesrcset="fooset" />
4396 <link
4397 rel="preload"
4398 as="image"
4399 imagesrcset="fooset"
4400 imagesizes="foosizes"
4401 />
4402 <link rel="preload" as="somethingelse" href="bar" />
4403 <link rel="preload" as="image" href="client" />
4404 <link rel="preload" as="image" imagesrcset="clientset" />
4405 <link
4406 rel="preload"
4407 as="image"
4408 imagesrcset="clientset"
4409 imagesizes="clientsizes"
4410 />
4411 </head>
4412 <body>hello</body>
4413 </html>,
4414 );
4415 });
4416
4417 it('should handle referrerPolicy on image preload', async () => {
4418 function App({isClient}) {
4419 ReactDOM.preload('/server', {
4420 as: 'image',
4421 imageSrcSet: '/server',
4422 imageSizes: '100vw',
4423 referrerPolicy: 'no-referrer',
4424 });
4425
4426 if (isClient) {
4427 ReactDOM.preload('/client', {
4428 as: 'image',
4429 imageSrcSet: '/client',
4430 imageSizes: '100vw',
4431 referrerPolicy: 'no-referrer',
4432 });
4433 }
4434
4435 return (
4436 <html>
4437 <body>hello</body>
4438 </html>
4439 );
4440 }
4441
4442 await act(() => {
4443 renderToPipeableStream(<App />).pipe(writable);
4444 });
4445 expect(getMeaningfulChildren(document)).toEqual(
4446 <html>
4447 <head>
4448 <link
4449 rel="preload"
4450 as="image"
4451 imagesrcset="/server"
4452 imagesizes="100vw"
4453 referrerpolicy="no-referrer"
4454 />
4455 </head>
4456 <body>hello</body>
4457 </html>,
4458 );
4459
4460 const root = ReactDOMClient.hydrateRoot(document, <App />);
4461 await waitForAll([]);
4462 expect(getMeaningfulChildren(document)).toEqual(
4463 <html>
4464 <head>
4465 <link
4466 rel="preload"
4467 as="image"
4468 imagesrcset="/server"
4469 imagesizes="100vw"
4470 referrerpolicy="no-referrer"
4471 />
4472 </head>
4473 <body>hello</body>
4474 </html>,
4475 );
4476
4477 root.render(<App isClient={true} />);
4478 await waitForAll([]);
4479 expect(getMeaningfulChildren(document)).toEqual(
4480 <html>
4481 <head>
4482 <link
4483 rel="preload"
4484 as="image"
4485 imagesrcset="/server"
4486 imagesizes="100vw"
4487 referrerpolicy="no-referrer"
4488 />
4489 <link
4490 rel="preload"
4491 as="image"
4492 imagesrcset="/client"
4493 imagesizes="100vw"
4494 referrerpolicy="no-referrer"
4495 />
4496 </head>
4497 <body>hello</body>
4498 </html>,
4499 );
4500 });
4501
4502 it('can emit preloads for non-lazy images that are rendered', async () => {
4503 function App() {
4504 ReactDOM.preload('script', {as: 'script'});
4505 ReactDOM.preload('a', {as: 'image'});
4506 ReactDOM.preload('b', {as: 'image'});
4507 return (
4508 <html>
4509 <body>
4510 <img src="a" />
4511 <img src="b" loading="lazy" />
4512 <img src="b2" loading="lazy" />
4513 <img src="c" srcSet="srcsetc" />
4514 <img src="d" srcSet="srcsetd" sizes="sizesd" />
4515 <img src="d" srcSet="srcsetd" sizes="sizesd2" />
4516 </body>
4517 </html>
4518 );
4519 }
4520
4521 await act(() => {
4522 renderToPipeableStream(<App />).pipe(writable);
4523 });
4524
4525 // non-lazy images are first, then arbitrary preloads like for the script and lazy images
4526 expect(getMeaningfulChildren(document)).toEqual(
4527 <html>
4528 <head>
4529 <link rel="preload" href="a" as="image" />
4530 <link rel="preload" as="image" imagesrcset="srcsetc" />
4531 <link
4532 rel="preload"
4533 as="image"
4534 imagesrcset="srcsetd"
4535 imagesizes="sizesd"
4536 />
4537 <link
4538 rel="preload"
4539 as="image"
4540 imagesrcset="srcsetd"
4541 imagesizes="sizesd2"
4542 />
4543 <link rel="preload" href="script" as="script" />
4544 <link rel="preload" href="b" as="image" />
4545 </head>
4546 <body>
4547 <img src="a" />
4548 <img src="b" loading="lazy" />
4549 <img src="b2" loading="lazy" />
4550 <img src="c" srcset="srcsetc" />
4551 <img src="d" srcset="srcsetd" sizes="sizesd" />
4552 <img src="d" srcset="srcsetd" sizes="sizesd2" />
4553 </body>
4554 </html>,
4555 );
4556 });
4557
4558 it('Does not preload lazy images', async () => {
4559 function App() {
4560 ReactDOM.preload('a', {as: 'image'});
4561 return (
4562 <html>
4563 <body>
4564 <img src="a" fetchPriority="low" />
4565 <img src="b" fetchPriority="low" />
4566 </body>
4567 </html>
4568 );
4569 }
4570 await act(() => {
4571 renderToPipeableStream(<App />).pipe(writable);
4572 });
4573
4574 expect(getMeaningfulChildren(document)).toEqual(
4575 <html>
4576 <head>
4577 <link rel="preload" as="image" href="a" />
4578 </head>
4579 <body>
4580 <img src="a" fetchpriority="low" />
4581 <img src="b" fetchpriority="low" />
4582 </body>
4583 </html>,
4584 );
4585 });
4586
4587 it('preloads up to 10 suspensey images as high priority when fetchPriority is not specified', async () => {
4588 function App() {
4589 ReactDOM.preload('1', {as: 'image', fetchPriority: 'high'});
4590 ReactDOM.preload('auto', {as: 'image'});
4591 ReactDOM.preload('low', {as: 'image', fetchPriority: 'low'});
4592 ReactDOM.preload('9', {as: 'image', fetchPriority: 'high'});
4593 ReactDOM.preload('10', {as: 'image', fetchPriority: 'high'});
4594 return (
4595 <html>
4596 <body>
4597 {/* skipping 1 */}
4598 <img src="2" />
4599 <img src="3" fetchPriority="auto" />
4600 <img src="4" fetchPriority="high" />
4601 <img src="5" />
4602 <img src="5low" fetchPriority="low" />
4603 <img src="6" />
4604 <img src="7" />
4605 <img src="8" />
4606 <img src="9" />
4607 {/* skipping 10 */}
4608 <img src="11" />
4609 <img src="12" fetchPriority="high" />
4610 </body>
4611 </html>
4612 );
4613 }
4614 await act(() => {
4615 renderToPipeableStream(<App />).pipe(writable);
4616 });
4617
4618 expect(getMeaningfulChildren(document)).toEqual(
4619 <html>
4620 <head>
4621 {/* First we see the preloads calls that made it to the high priority image queue */}
4622 <link rel="preload" as="image" href="1" fetchpriority="high" />
4623 <link rel="preload" as="image" href="9" fetchpriority="high" />
4624 <link rel="preload" as="image" href="10" fetchpriority="high" />
4625 {/* Next we see up to 7 more images qualify for high priority image queue */}
4626 <link rel="preload" as="image" href="2" />
4627 <link rel="preload" as="image" href="3" fetchpriority="auto" />
4628 <link rel="preload" as="image" href="4" fetchpriority="high" />
4629 <link rel="preload" as="image" href="5" />
4630 <link rel="preload" as="image" href="6" />
4631 <link rel="preload" as="image" href="7" />
4632 <link rel="preload" as="image" href="8" />
4633 {/* Next we see images that are explicitly high priority and thus make it to the high priority image queue */}
4634 <link rel="preload" as="image" href="12" fetchpriority="high" />
4635 {/* Next we see the remaining preloads that did not make it to the high priority image queue */}
4636 <link rel="preload" as="image" href="auto" />
4637 <link rel="preload" as="image" href="low" fetchpriority="low" />
4638 <link rel="preload" as="image" href="11" />
4639 </head>
4640 <body>
4641 {/* skipping 1 */}
4642 <img src="2" />
4643 <img src="3" fetchpriority="auto" />
4644 <img src="4" fetchpriority="high" />
4645 <img src="5" />
4646 <img src="5low" fetchpriority="low" />
4647 <img src="6" />
4648 <img src="7" />
4649 <img src="8" />
4650 <img src="9" />
4651 {/* skipping 10 */}
4652 <img src="11" />
4653 <img src="12" fetchpriority="high" />
4654 </body>
4655 </html>,
4656 );
4657 });
4658
4659 it('can promote images to high priority when at least one instance specifies a high fetchPriority', async () => {
4660 function App() {
4661 // If a ends up in a higher priority queue than b it will flush first
4662 ReactDOM.preload('a', {as: 'image'});
4663 ReactDOM.preload('b', {as: 'image'});
4664 return (
4665 <html>
4666 <body>
4667 <link rel="stylesheet" href="foo" precedence="default" />
4668 <img src="1" />
4669 <img src="2" />
4670 <img src="3" />
4671 <img src="4" />
4672 <img src="5" />
4673 <img src="6" />
4674 <img src="7" />
4675 <img src="8" />
4676 <img src="9" />
4677 <img src="10" />
4678 <img src="11" />
4679 <img src="12" />
4680 <img src="a" fetchPriority="low" />
4681 <img src="a" />
4682 <img src="a" fetchPriority="high" />
4683 <img src="a" />
4684 <img src="a" />
4685 </body>
4686 </html>
4687 );
4688 }
4689
4690 await act(() => {
4691 renderToPipeableStream(<App />).pipe(writable);
4692 });
4693 expect(getMeaningfulChildren(document)).toEqual(
4694 <html>
4695 <head>
4696 {/* The First 10 high priority images were just the first 10 rendered images */}
4697 <link rel="preload" as="image" href="1" />
4698 <link rel="preload" as="image" href="2" />
4699 <link rel="preload" as="image" href="3" />
4700 <link rel="preload" as="image" href="4" />
4701 <link rel="preload" as="image" href="5" />
4702 <link rel="preload" as="image" href="6" />
4703 <link rel="preload" as="image" href="7" />
4704 <link rel="preload" as="image" href="8" />
4705 <link rel="preload" as="image" href="9" />
4706 <link rel="preload" as="image" href="10" />
4707 {/* The "a" image was rendered a few times but since at least one of those was with
4708 fetchPriorty="high" it ends up in the high priority queue */}
4709 <link rel="preload" as="image" href="a" />
4710 {/* Stylesheets come in between high priority images and regular preloads */}
4711 <link rel="stylesheet" href="foo" data-precedence="default" />
4712 {/* The remainig images that preloaded at regular priority */}
4713 <link rel="preload" as="image" href="b" />
4714 <link rel="preload" as="image" href="11" />
4715 <link rel="preload" as="image" href="12" />
4716 </head>
4717 <body>
4718 <img src="1" />
4719 <img src="2" />
4720 <img src="3" />
4721 <img src="4" />
4722 <img src="5" />
4723 <img src="6" />
4724 <img src="7" />
4725 <img src="8" />
4726 <img src="9" />
4727 <img src="10" />
4728 <img src="11" />
4729 <img src="12" />
4730 <img src="a" fetchpriority="low" />
4731 <img src="a" />
4732 <img src="a" fetchpriority="high" />
4733 <img src="a" />
4734 <img src="a" />
4735 </body>
4736 </html>,
4737 );
4738 });
4739
4740 it('preloads from rendered images properly use srcSet and sizes', async () => {
4741 function App() {
4742 ReactDOM.preload('1', {as: 'image', imageSrcSet: 'ss1'});
4743 ReactDOM.preload('2', {
4744 as: 'image',
4745 imageSrcSet: 'ss2',
4746 imageSizes: 's2',
4747 });
4748 return (
4749 <html>
4750 <body>
4751 <img src="1" srcSet="ss1" />
4752 <img src="2" srcSet="ss2" sizes="s2" />
4753 <img src="3" srcSet="ss3" />
4754 <img src="4" srcSet="ss4" sizes="s4" />
4755 </body>
4756 </html>
4757 );
4758 }
4759 await act(() => {
4760 renderToPipeableStream(<App />).pipe(writable);
4761 });
4762
4763 expect(getMeaningfulChildren(document)).toEqual(
4764 <html>
4765 <head>
4766 <link rel="preload" as="image" imagesrcset="ss1" />
4767 <link rel="preload" as="image" imagesrcset="ss2" imagesizes="s2" />
4768 <link rel="preload" as="image" imagesrcset="ss3" />
4769 <link rel="preload" as="image" imagesrcset="ss4" imagesizes="s4" />
4770 </head>
4771 <body>
4772 <img src="1" srcset="ss1" />
4773 <img src="2" srcset="ss2" sizes="s2" />
4774 <img src="3" srcset="ss3" />
4775 <img src="4" srcset="ss4" sizes="s4" />
4776 </body>
4777 </html>,
4778 );
4779 });
4780
4781 it('should not preload images that have a data URIs for src or srcSet', async () => {
4782 function App() {
4783 return (
4784 <html>
4785 <body>
4786 <img src="data:1" />
4787 <img src="data:2" srcSet="ss2" />
4788 <img srcSet="data:3a, data:3b 2x" />
4789 <img src="4" srcSet="data:4a, data4b 2x" />
4790 </body>
4791 </html>
4792 );
4793 }
4794 await act(() => {
4795 renderToPipeableStream(<App />).pipe(writable);
4796 });
4797
4798 expect(getMeaningfulChildren(document)).toEqual(
4799 <html>
4800 <head />
4801 <body>
4802 <img src="data:1" />
4803 <img src="data:2" srcset="ss2" />
4804 <img srcset="data:3a, data:3b 2x" />
4805 <img src="4" srcset="data:4a, data4b 2x" />
4806 </body>
4807 </html>,
4808 );
4809 });
4810
4811 // https://github.com/vercel/next.js/discussions/54799
4812 it('omits preloads when an <img> is inside a <picture>', async () => {
4813 await act(() => {
4814 renderToPipeableStream(
4815 <html>
4816 <body>
4817 <picture>
4818 <img src="foo" />
4819 </picture>
4820 <picture>
4821 <source type="image/webp" srcSet="webpsrc" />
4822 <img src="jpg fallback" />
4823 </picture>
4824 </body>
4825 </html>,
4826 ).pipe(writable);
4827 });
4828
4829 expect(getMeaningfulChildren(document)).toEqual(
4830 <html>
4831 <head />
4832 <body>
4833 <picture>
4834 <img src="foo" />
4835 </picture>
4836 <picture>
4837 <source type="image/webp" srcset="webpsrc" />
4838 <img src="jpg fallback" />
4839 </picture>
4840 </body>
4841 </html>,
4842 );
4843 });
4844
4845 // Fixes: https://github.com/facebook/react/issues/27910
4846 it('omits preloads for images inside noscript tags', async () => {
4847 function App() {
4848 return (
4849 <html>
4850 <body>
4851 <img src="foo" />
4852 <noscript>
4853 <img src="bar" />
4854 </noscript>
4855 </body>
4856 </html>
4857 );
4858 }
4859
4860 await act(() => {
4861 renderToPipeableStream(<App />).pipe(writable);
4862 });
4863
4864 expect(getMeaningfulChildren(document)).toEqual(
4865 <html>
4866 <head>
4867 <link rel="preload" href="foo" as="image" />
4868 </head>
4869 <body>
4870 <img src="foo" />
4871 <noscript>&lt;img src="bar"&gt;</noscript>
4872 </body>
4873 </html>,
4874 );
4875 });
4876
4877 it('should handle media on image preload', async () => {
4878 function App({isClient}) {
4879 ReactDOM.preload('/server', {
4880 as: 'image',
4881 imageSrcSet: '/server',
4882 imageSizes: '100vw',
4883 media: 'print and (min-width: 768px)',
4884 });
4885
4886 if (isClient) {
4887 ReactDOM.preload('/client', {
4888 as: 'image',
4889 imageSrcSet: '/client',
4890 imageSizes: '100vw',
4891 media: 'screen and (max-width: 480px)',
4892 });
4893 }
4894
4895 return (
4896 <html>
4897 <body>hello</body>
4898 </html>
4899 );
4900 }
4901
4902 await act(() => {
4903 renderToPipeableStream(<App />).pipe(writable);
4904 });
4905 expect(getMeaningfulChildren(document)).toEqual(
4906 <html>
4907 <head>
4908 <link
4909 rel="preload"
4910 as="image"
4911 imagesrcset="/server"
4912 imagesizes="100vw"
4913 media="print and (min-width: 768px)"
4914 />
4915 </head>
4916 <body>hello</body>
4917 </html>,
4918 );
4919
4920 const root = ReactDOMClient.hydrateRoot(document, <App />);
4921 await waitForAll([]);
4922 expect(getMeaningfulChildren(document)).toEqual(
4923 <html>
4924 <head>
4925 <link
4926 rel="preload"
4927 as="image"
4928 imagesrcset="/server"
4929 imagesizes="100vw"
4930 media="print and (min-width: 768px)"
4931 />
4932 </head>
4933 <body>hello</body>
4934 </html>,
4935 );
4936
4937 root.render(<App isClient={true} />);
4938 await waitForAll([]);
4939 expect(getMeaningfulChildren(document)).toEqual(
4940 <html>
4941 <head>
4942 <link
4943 rel="preload"
4944 as="image"
4945 imagesrcset="/server"
4946 imagesizes="100vw"
4947 media="print and (min-width: 768px)"
4948 />
4949 <link
4950 rel="preload"
4951 as="image"
4952 imagesrcset="/client"
4953 imagesizes="100vw"
4954 media="screen and (max-width: 480px)"
4955 />
4956 </head>
4957 <body>hello</body>
4958 </html>,
4959 );
4960 });
4961
4962 it('should warn if you preload a stylesheet and then render a style tag with the same href', async () => {
4963 const style = 'body { color: red; }';
4964 function App() {
4965 ReactDOM.preload('foo', {as: 'style'});
4966 return (
4967 <html>
4968 <body>
4969 hello
4970 <style precedence="default" href="foo">
4971 {style}
4972 </style>
4973 </body>
4974 </html>
4975 );
4976 }
4977
4978 await act(() => {
4979 renderToPipeableStream(<App />).pipe(writable);
4980 });
4981 assertConsoleErrorDev([
4982 'React encountered a hoistable style tag for the same href as a preload: "foo". ' +
4983 'When using a style tag to inline styles you should not also preload it as a stylsheet.\n' +
4984 ' in style (at **)\n' +
4985 ' in App (at **)',
4986 ]);
4987
4988 expect(getMeaningfulChildren(document)).toEqual(
4989 <html>
4990 <head>
4991 <style data-precedence="default" data-href="foo">
4992 {style}
4993 </style>
4994 <link rel="preload" as="style" href="foo" />
4995 </head>
4996 <body>hello</body>
4997 </html>,
4998 );
4999 });
5000
Showing first 5,000 of 10,364 lines. View raw