39
const jsdom = new JSDOM(markup, {
40
runScripts: 'dangerously',
41
});
42
+ // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else
43
+ Object.defineProperty(jsdom.window, 'matchMedia', {
44
+ writable: true,
45
+ value: jest.fn().mockImplementation(query => ({
46
+ matches: query === 'all' || query === '',
47
+ media: query,
48
+ })),
49
+ });
50
window = jsdom.window;
51
document = jsdom.window.document;
52
}
88
}
89
});
90
83
- function normalizeCodeLocInfo(str) {
84
- return (
85
- typeof str === 'string' &&
86
- str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
87
- return '\n in ' + name + ' (at **)';
88
- })
89
- );
90
- }
91
-
92
- function componentStack(components) {
93
- return components
94
- .map(component => `\n in ${component} (at **)`)
95
- .join('');
96
- }
97
-
91
async function act(callback) {
92
await callback();
93
// Await one turn around the event loop.
189
: children;
190
}
191
192
+ function BlockedOn({value, children}) {
193
+ readText(value);
194
+ return children;
195
+ }
196
+
197
function resolveText(text) {
198
const record = textCache.get(text);
199
if (record === undefined) {
255
);
256
}
257
258
+ function renderSafelyAndExpect(root, children) {
259
+ root.render(children);
260
+ return expect(() => {
261
+ try {
262
+ expect(Scheduler).toFlushWithoutYielding();
263
+ } catch (e) {
264
+ try {
265
+ expect(Scheduler).toFlushWithoutYielding();
266
+ } catch (f) {}
267
+ }
268
+ });
269
+ }
270
+
271
// @gate enableFloat
272
it('can render resources before singletons', async () => {
273
const root = ReactDOMClient.createRoot(document);
301
);
302
});
303
293
- function renderSafelyAndExpect(root, children) {
294
- root.render(children);
295
- return expect(() => {
296
- try {
297
- expect(Scheduler).toFlushWithoutYielding();
298
- } catch (e) {
299
- try {
300
- expect(Scheduler).toFlushWithoutYielding();
301
- } catch (f) {}
302
- }
303
- });
304
- }
305
-
304
// @gate enableFloat
305
it('can hydrate non Resources in head when Resources are also inserted there', async () => {
306
await actIntoEmptyDocument(() => {
324
expect(getMeaningfulChildren(document)).toEqual(
325
<html>
326
<head>
329
- <base target="foo" href="bar" />
327
<link rel="preload" href="foo" as="script" />
328
<meta property="foo" content="bar" />
329
<title>foo</title>
330
+ <link rel="foo" href="bar" />
331
<noscript><link rel="icon" href="icon"/></noscript>
332
+ <base target="foo" href="bar" />
333
</head>
334
<body>foo</body>
335
</html>,
355
expect(getMeaningfulChildren(document)).toEqual(
356
<html>
357
<head>
359
- <base target="foo" href="bar" />
358
<link rel="preload" href="foo" as="script" />
359
<meta property="foo" content="bar" />
360
<title>foo</title>
361
<link rel="foo" href="bar" />
362
+ <link rel="foo" href="bar" />
363
<noscript><link rel="icon" href="icon"/></noscript>
364
+ <base target="foo" href="bar" />
365
<script async="" src="foo" />
366
</head>
367
<body>foo</body>
407
<style>foo</style>
408
</html>,
409
).toErrorDev([
410
- 'Cannot render <style> outside the main document. Try moving it into the root <head> tag.',
410
+ 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key. React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. Note that hoisting <style> tags is considered an advanced feature that most will not use directly. Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style> to the <style> tag.',
411
'Warning: validateDOMNesting(...): <style> cannot appear as a child of <html>.',
412
]);
413
473
const root = ReactDOMClient.createRoot(container);
474
root.render(
475
<>
476
- <title>foo</title>
476
+ <script async={true} src="foo" />
477
</>,
478
);
479
expect(Scheduler).toFlushWithoutYielding();
480
expect(getMeaningfulChildren(document)).toEqual(
481
<html>
482
<head>
483
- <title>foo</title>
483
+ <script async="" src="foo" />
484
</head>
485
<body>
486
<div id="container" />
488
</html>,
489
);
490
491
- // title is keyed off children so this second resource should match the first one
491
root.render(
492
<>
493
{null}
495
- <title data-new="new">foo</title>
494
+ <script data-new="new" async={true} src="foo" />
495
</>,
496
);
497
expect(Scheduler).toFlushWithoutYielding();
499
expect(getMeaningfulChildren(document)).toEqual(
500
<html>
501
<head>
503
- <title>foo</title>
502
+ <script async="" src="foo" />
503
</head>
504
<body>
505
<div id="container" />
508
);
509
});
510
512
- // @gate enableFloat
513
- it('errors if the document does not contain a head when inserting a resource', async () => {
514
- document.head.parentNode.removeChild(document.head);
515
- const root = ReactDOMClient.createRoot(document);
516
- root.render(
517
- <html>
518
- <body>
519
- <link rel="stylesheet" href="foo" precedence="default" />
520
- foo
521
- </body>
522
- </html>,
523
- );
524
- expect(() => {
525
- expect(Scheduler).toFlushWithoutYielding();
526
- }).toThrow(
527
- 'While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.',
528
- );
529
- });
530
-
511
// @gate enableFloat
512
it('emits resources before everything else when rendering with no head', async () => {
513
function App() {
563
});
564
565
// @gate enableFloat
586
- it('does not emit closing tags in out of order position when rendering a non-void resource type', async () => {
566
+ it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {
567
const chunks = [];
568
569
writable.on('data', chunk => {
583
pipe(writable);
584
});
585
expect(chunks).toEqual([
606
- '<!DOCTYPE html><html><script async="" src="foo"></script><title>foo</title><body>bar',
586
+ '<!DOCTYPE html><html><head><script async="" src="foo"></script><title>foo</title></head><body>bar',
587
'</body></html>',
588
]);
589
});
622
).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
623
});
624
645
- describe('HostResource', () => {
646
- // @gate enableFloat
647
- it('warns when you update props to an invalid type', async () => {
648
- const root = ReactDOMClient.createRoot(container);
649
- root.render(
650
- <div>
651
- <link rel="stylesheet" href="foo" precedence="foo" />
652
- <link rel="stylesheet" href="bar" precedence="foo" />
653
- </div>,
654
- );
655
- expect(Scheduler).toFlushWithoutYielding();
656
- root.render(
657
- <div>
658
- <link rel={() => {}} href="bar" />
659
- <link rel="foo" href={() => {}} />
660
- </div>,
661
- );
662
- expect(() => {
663
- expect(Scheduler).toFlushWithoutYielding();
664
- }).toErrorDev([
665
- 'Warning: A <link> previously rendered as a Resource with href "foo" with rel ""stylesheet"" but was updated with an invalid rel: something with type "function". When a link does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
666
- 'Warning: A <link> previously rendered as a Resource with href "bar" but was updated with an invalid href prop: something with type "function". When a link does not have a valid href prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
667
- ]);
668
- expect(getMeaningfulChildren(document)).toEqual(
625
+ // @gate enableFloat
626
+ it('can avoid inserting a late stylesheet if it already rendered on the client', async () => {
627
+ await actIntoEmptyDocument(() => {
628
+ renderToPipeableStream(
629
<html>
670
- <head>
671
- <link rel="stylesheet" href="foo" data-precedence="foo" />
672
- <link rel="stylesheet" href="bar" data-precedence="foo" />
673
- <link rel="preload" as="style" href="foo" />
674
- <link rel="preload" as="style" href="bar" />
675
- </head>
630
<body>
677
- <div id="container">
678
- <div />
679
- </div>
631
+ <Suspense fallback="loading foo...">
632
+ <BlockedOn value="foo">
633
+ <link rel="stylesheet" href="foo" precedence="foo" />
634
+ foo
635
+ </BlockedOn>
636
+ </Suspense>
637
+ <Suspense fallback="loading bar...">
638
+ <BlockedOn value="bar">
639
+ <link rel="stylesheet" href="bar" precedence="bar" />
640
+ bar
641
+ </BlockedOn>
642
+ </Suspense>
643
</body>
644
</html>,
682
- );
645
+ ).pipe(writable);
646
});
684
- });
647
686
- describe('ReactDOM.preload', () => {
687
- // @gate enableFloat
688
- it('inserts a preload resource into the stream when called during server rendering', async () => {
689
- function Component() {
690
- ReactDOM.preload('foo', {as: 'style'});
691
- return 'foo';
692
- }
693
- await actIntoEmptyDocument(() => {
694
- const {pipe} = renderToPipeableStream(
695
- <html>
696
- <head />
697
- <body>
698
- <Component />
699
- </body>
700
- </html>,
701
- );
702
- pipe(writable);
703
- });
704
- expect(getMeaningfulChildren(document)).toEqual(
705
- <html>
706
- <head>
707
- <link rel="preload" as="style" href="foo" />
708
- </head>
709
- <body>foo</body>
710
- </html>,
648
+ expect(getMeaningfulChildren(document)).toEqual(
649
+ <html>
650
+ <head />
651
+ <body>
652
+ {'loading foo...'}
653
+ {'loading bar...'}
654
+ </body>
655
+ </html>,
656
+ );
657
+
658
+ ReactDOMClient.hydrateRoot(
659
+ document,
660
+ <html>
661
+ <body>
662
+ <link rel="stylesheet" href="foo" precedence="foo" />
663
+ <Suspense fallback="loading foo...">
664
+ <link rel="stylesheet" href="foo" precedence="foo" />
665
+ foo
666
+ </Suspense>
667
+ <Suspense fallback="loading bar...">
668
+ <link rel="stylesheet" href="bar" precedence="bar" />
669
+ bar
670
+ </Suspense>
671
+ </body>
672
+ </html>,
673
+ );
674
+ expect(Scheduler).toFlushWithoutYielding();
675
+ expect(getMeaningfulChildren(document)).toEqual(
676
+ <html>
677
+ <head>
678
+ <link rel="stylesheet" href="foo" data-precedence="foo" />
679
+ <link as="style" href="foo" rel="preload" />
680
+ </head>
681
+ <body>
682
+ {'loading foo...'}
683
+ {'loading bar...'}
684
+ </body>
685
+ </html>,
686
+ );
687
+
688
+ await act(() => {
689
+ resolveText('bar');
690
+ });
691
+ await act(() => {
692
+ const sheets = document.querySelectorAll(
693
+ 'link[rel="stylesheet"][data-precedence]',
694
);
695
+ const event = document.createEvent('Event');
696
+ event.initEvent('load', true, true);
697
+ for (let i = 0; i < sheets.length; i++) {
698
+ sheets[i].dispatchEvent(event);
699
+ }
700
});
701
+ expect(getMeaningfulChildren(document)).toEqual(
702
+ <html>
703
+ <head>
704
+ <link rel="stylesheet" href="foo" data-precedence="foo" />
705
+ <link rel="stylesheet" href="bar" data-precedence="bar" />
706
+ <link as="style" href="foo" rel="preload" />
707
+ </head>
708
+ <body>
709
+ {'loading foo...'}
710
+ {'bar'}
711
+ <link as="style" href="bar" rel="preload" />
712
+ </body>
713
+ </html>,
714
+ );
715
714
- // @gate enableFloat
715
- it('inserts a preload resource into the document during render when called during client rendering', async () => {
716
- function Component() {
717
- ReactDOM.preload('foo', {as: 'style'});
718
- return 'foo';
719
- }
720
- const root = ReactDOMClient.createRoot(container);
721
- root.render(<Component />);
722
- expect(Scheduler).toFlushWithoutYielding();
723
- expect(getMeaningfulChildren(document)).toEqual(
724
- <html>
725
- <head>
726
- <link rel="preload" as="style" href="foo" />
727
- </head>
728
- <body>
729
- <div id="container">foo</div>
730
- </body>
731
- </html>,
716
+ await act(() => {
717
+ resolveText('foo');
718
+ });
719
+ await act(() => {
720
+ const sheets = document.querySelectorAll(
721
+ 'link[rel="stylesheet"][data-precedence]',
722
);
723
+ const event = document.createEvent('Event');
724
+ event.initEvent('load', true, true);
725
+ for (let i = 0; i < sheets.length; i++) {
726
+ sheets[i].dispatchEvent(event);
727
+ }
728
});
729
+ expect(getMeaningfulChildren(document)).toEqual(
730
+ <html>
731
+ <head>
732
+ <link rel="stylesheet" href="foo" data-precedence="foo" />
733
+ <link rel="stylesheet" href="bar" data-precedence="bar" />
734
+ <link as="style" href="foo" rel="preload" />
735
+ </head>
736
+ <body>
737
+ {'foo'}
738
+ {'bar'}
739
+ <link as="style" href="bar" rel="preload" />
740
+ <link as="style" href="foo" rel="preload" />
741
+ </body>
742
+ </html>,
743
+ );
744
+ });
745
735
- // @gate enableFloat
736
- it('inserts a preload resource when called in a layout effect', async () => {
737
- function App() {
738
- React.useLayoutEffect(() => {
739
- ReactDOM.preload('foo', {as: 'style'});
740
- }, []);
741
- return 'foobar';
742
- }
743
- const root = ReactDOMClient.createRoot(container);
744
- root.render(<App />);
745
- expect(Scheduler).toFlushWithoutYielding();
746
+ // @gate enableFloat
747
+ it('can hoist <link rel="stylesheet" .../> and <style /> tags together, respecting order of discovery', async () => {
748
+ const css = `
749
+body {
750
+ background-color: red;
751
+}`;
752
747
- expect(getMeaningfulChildren(document)).toEqual(
753
+ await actIntoEmptyDocument(() => {
754
+ renderToPipeableStream(
755
<html>
749
- <head>
750
- <link rel="preload" as="style" href="foo" />
751
- </head>
756
<body>
753
- <div id="container">foobar</div>
757
+ <link rel="stylesheet" href="one1" precedence="one" />
758
+ <style href="two1" precedence="two">
759
+ {css}
760
+ </style>
761
+ <link rel="stylesheet" href="three1" precedence="three" />
762
+ <style href="four1" precedence="four">
763
+ {css}
764
+ </style>
765
+ <Suspense>
766
+ <BlockedOn value="block">
767
+ <link rel="stylesheet" href="one2" precedence="one" />
768
+ <link rel="stylesheet" href="two2" precedence="two" />
769
+ <style href="three2" precedence="three">
770
+ {css}
771
+ </style>
772
+ <style href="four2" precedence="four">
773
+ {css}
774
+ </style>
775
+ <link rel="stylesheet" href="five1" precedence="five" />
776
+ </BlockedOn>
777
+ </Suspense>
778
+ <Suspense>
779
+ <BlockedOn value="block2">
780
+ <style href="one3" precedence="one">
781
+ {css}
782
+ </style>
783
+ <style href="two3" precedence="two">
784
+ {css}
785
+ </style>
786
+ <link rel="stylesheet" href="three3" precedence="three" />
787
+ <link rel="stylesheet" href="four3" precedence="four" />
788
+ <style href="six1" precedence="six">
789
+ {css}
790
+ </style>
791
+ </BlockedOn>
792
+ </Suspense>
793
+ <Suspense>
794
+ <BlockedOn value="block again">
795
+ <link rel="stylesheet" href="one2" precedence="one" />
796
+ <link rel="stylesheet" href="two2" precedence="two" />
797
+ <style href="three2" precedence="three">
798
+ {css}
799
+ </style>
800
+ <style href="four2" precedence="four">
801
+ {css}
802
+ </style>
803
+ <link rel="stylesheet" href="five1" precedence="five" />
804
+ </BlockedOn>
805
+ </Suspense>
806
</body>
807
</html>,
756
- );
808
+ ).pipe(writable);
809
});
810
759
- // @gate enableFloat
760
- it('inserts a preload resource when called in a passive effect', async () => {
761
- function App() {
762
- React.useEffect(() => {
763
- ReactDOM.preload('foo', {as: 'style'});
764
- }, []);
765
- return 'foobar';
766
- }
767
- const root = ReactDOMClient.createRoot(container);
768
- root.render(<App />);
769
- expect(Scheduler).toFlushWithoutYielding();
811
+ expect(getMeaningfulChildren(document)).toEqual(
812
+ <html>
813
+ <head>
814
+ <link rel="stylesheet" href="one1" data-precedence="one" />
815
+ <style data-href="two1" data-precedence="two">
816
+ {css}
817
+ </style>
818
+ <link rel="stylesheet" href="three1" data-precedence="three" />
819
+ <style data-href="four1" data-precedence="four">
820
+ {css}
821
+ </style>
822
+ </head>
823
+ <body />
824
+ </html>,
825
+ );
826
771
- expect(getMeaningfulChildren(document)).toEqual(
772
- <html>
773
- <head>
774
- <link rel="preload" as="style" href="foo" />
775
- </head>
776
- <body>
777
- <div id="container">foobar</div>
778
- </body>
779
- </html>,
780
- );
827
+ await act(() => {
828
+ resolveText('block');
829
});
830
783
- // @gate enableFloat
784
- it('inserts a preload resource when called in module scope if a root has already been created', async () => {
785
- // The requirement that a root be created has to do with bootstrapping the dispatcher.
786
- // We are intentionally avoiding setting it to the default via import due to cycles and
787
- // we are trying to avoid doing a mutable initailation in module scope.
788
- ReactDOM.preload('foo', {as: 'style'});
789
- ReactDOMClient.createRoot(container);
790
- ReactDOM.preload('bar', {as: 'style'});
791
- // We need to use global.document because preload falls back
792
- // to the window.document global when no other documents have been used
793
- // The way the JSDOM runtim is created for these tests the local document
794
- // global does not point to the global.document
795
- expect(getMeaningfulChildren(global.document)).toEqual(
796
- <html>
797
- <head>
798
- <link rel="preload" as="style" href="bar" />
799
- </head>
800
- <body />
801
- </html>,
802
- );
831
+ expect(getMeaningfulChildren(document)).toEqual(
832
+ <html>
833
+ <head>
834
+ <link rel="stylesheet" href="one1" data-precedence="one" />
835
+ <link rel="stylesheet" href="one2" data-precedence="one" />
836
+ <style data-href="two1" data-precedence="two">
837
+ {css}
838
+ </style>
839
+ <link rel="stylesheet" href="two2" data-precedence="two" />
840
+ <link rel="stylesheet" href="three1" data-precedence="three" />
841
+ <style data-href="three2" data-precedence="three">
842
+ {css}
843
+ </style>
844
+ <style data-href="four1" data-precedence="four">
845
+ {css}
846
+ </style>
847
+ <style data-href="four2" data-precedence="four">
848
+ {css}
849
+ </style>
850
+ <link rel="stylesheet" href="five1" data-precedence="five" />
851
+ </head>
852
+ <body>
853
+ <link rel="preload" href="one2" as="style" />
854
+ <link rel="preload" href="two2" as="style" />
855
+ <link rel="preload" href="five1" as="style" />
856
+ </body>
857
+ </html>,
858
+ );
859
+
860
+ await act(() => {
861
+ resolveText('block2');
862
});
863
805
- // @gate enableFloat
806
- it('supports script preloads', async () => {
807
- function ServerApp() {
808
- ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
809
- ReactDOM.preload('bar', {
810
- as: 'script',
811
- crossOrigin: 'use-credentials',
812
- integrity: 'bar hash',
813
- });
814
- return (
815
- <html>
816
- <link rel="preload" href="baz" as="script" />
817
- <head>
818
- <title>hi</title>
819
- </head>
820
- <body>foo</body>
821
- </html>
822
- );
823
- }
824
- function ClientApp() {
825
- ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
826
- ReactDOM.preload('qux', {as: 'script'});
827
- return (
828
- <html>
829
- <head>
830
- <title>hi</title>
831
- </head>
832
- <body>foo</body>
833
- <link
834
- rel="preload"
835
- href="quux"
836
- as="script"
837
- crossOrigin=""
838
- integrity="quux hash"
839
- />
840
- </html>
841
- );
842
- }
864
+ expect(getMeaningfulChildren(document)).toEqual(
865
+ <html>
866
+ <head>
867
+ <link rel="stylesheet" href="one1" data-precedence="one" />
868
+ <link rel="stylesheet" href="one2" data-precedence="one" />
869
+ <style data-href="one3" data-precedence="one">
870
+ {css}
871
+ </style>
872
+ <style data-href="two1" data-precedence="two">
873
+ {css}
874
+ </style>
875
+ <link rel="stylesheet" href="two2" data-precedence="two" />
876
+ <style data-href="two3" data-precedence="two">
877
+ {css}
878
+ </style>
879
+ <link rel="stylesheet" href="three1" data-precedence="three" />
880
+ <style data-href="three2" data-precedence="three">
881
+ {css}
882
+ </style>
883
+ <link rel="stylesheet" href="three3" data-precedence="three" />
884
+ <style data-href="four1" data-precedence="four">
885
+ {css}
886
+ </style>
887
+ <style data-href="four2" data-precedence="four">
888
+ {css}
889
+ </style>
890
+ <link rel="stylesheet" href="four3" data-precedence="four" />
891
+ <link rel="stylesheet" href="five1" data-precedence="five" />
892
+ <style data-href="six1" data-precedence="six">
893
+ {css}
894
+ </style>
895
+ </head>
896
+ <body>
897
+ <link rel="preload" href="one2" as="style" />
898
+ <link rel="preload" href="two2" as="style" />
899
+ <link rel="preload" href="five1" as="style" />
900
+ <link rel="preload" href="three3" as="style" />
901
+ <link rel="preload" href="four3" as="style" />
902
+ </body>
903
+ </html>,
904
+ );
905
844
- await actIntoEmptyDocument(() => {
845
- const {pipe} = renderToPipeableStream(<ServerApp />);
846
- pipe(writable);
847
- });
848
- expect(getMeaningfulChildren(document)).toEqual(
849
- <html>
850
- <head>
851
- <link rel="preload" as="script" href="foo" integrity="foo hash" />
852
- <link
853
- rel="preload"
854
- as="script"
855
- href="bar"
856
- crossorigin="use-credentials"
857
- integrity="bar hash"
858
- />
859
- <link rel="preload" as="script" href="baz" />
860
- <title>hi</title>
861
- </head>
862
- <body>foo</body>
863
- </html>,
864
- );
906
+ await act(() => {
907
+ resolveText('block again');
908
+ });
909
866
- ReactDOMClient.hydrateRoot(document, <ClientApp />);
867
- expect(Scheduler).toFlushWithoutYielding();
910
+ expect(getMeaningfulChildren(document)).toEqual(
911
+ <html>
912
+ <head>
913
+ <link rel="stylesheet" href="one1" data-precedence="one" />
914
+ <link rel="stylesheet" href="one2" data-precedence="one" />
915
+ <style data-href="one3" data-precedence="one">
916
+ {css}
917
+ </style>
918
+ <style data-href="two1" data-precedence="two">
919
+ {css}
920
+ </style>
921
+ <link rel="stylesheet" href="two2" data-precedence="two" />
922
+ <style data-href="two3" data-precedence="two">
923
+ {css}
924
+ </style>
925
+ <link rel="stylesheet" href="three1" data-precedence="three" />
926
+ <style data-href="three2" data-precedence="three">
927
+ {css}
928
+ </style>
929
+ <link rel="stylesheet" href="three3" data-precedence="three" />
930
+ <style data-href="four1" data-precedence="four">
931
+ {css}
932
+ </style>
933
+ <style data-href="four2" data-precedence="four">
934
+ {css}
935
+ </style>
936
+ <link rel="stylesheet" href="four3" data-precedence="four" />
937
+ <link rel="stylesheet" href="five1" data-precedence="five" />
938
+ <style data-href="six1" data-precedence="six">
939
+ {css}
940
+ </style>
941
+ </head>
942
+ <body>
943
+ <link rel="preload" href="one2" as="style" />
944
+ <link rel="preload" href="two2" as="style" />
945
+ <link rel="preload" href="five1" as="style" />
946
+ <link rel="preload" href="three3" as="style" />
947
+ <link rel="preload" href="four3" as="style" />
948
+ </body>
949
+ </html>,
950
+ );
951
869
- expect(getMeaningfulChildren(document)).toEqual(
870
- <html>
871
- <head>
872
- <link rel="preload" as="script" href="foo" integrity="foo hash" />
873
- <link
874
- rel="preload"
875
- as="script"
876
- href="bar"
877
- crossorigin="use-credentials"
878
- integrity="bar hash"
879
- />
880
- <link rel="preload" as="script" href="baz" />
881
- <title>hi</title>
882
- <link rel="preload" as="script" href="qux" />
883
- <link
884
- rel="preload"
885
- as="script"
886
- href="quux"
887
- crossorigin=""
888
- integrity="quux hash"
889
- />
890
- </head>
891
- <body>foo</body>
892
- </html>,
893
- );
894
- });
895
- });
952
+ ReactDOMClient.hydrateRoot(
953
+ document,
954
+ <html>
955
+ <body>
956
+ <link rel="stylesheet" href="one4" precedence="one" />
957
+ <style href="two4" precedence="two">
958
+ {css}
959
+ </style>
960
+ <link rel="stylesheet" href="three4" precedence="three" />
961
+ <style href="four4" precedence="four">
962
+ {css}
963
+ </style>
964
+ <link rel="stylesheet" href="seven1" precedence="seven" />
965
+ <style href="eight1" precedence="eight">
966
+ {css}
967
+ </style>
968
+ </body>
969
+ </html>,
970
+ );
971
+ expect(Scheduler).toFlushWithoutYielding();
972
897
- describe('ReactDOM.preinit as style', () => {
898
- // @gate enableFloat
899
- it('creates a style Resource when called during server rendering before first flush', async () => {
900
- function Component() {
901
- ReactDOM.preinit('foo', {as: 'style'});
902
- return 'foo';
903
- }
904
- await actIntoEmptyDocument(() => {
905
- const {pipe} = renderToPipeableStream(
906
- <html>
907
- <head />
908
- <body>
909
- <Component />
910
- </body>
911
- </html>,
912
- );
913
- pipe(writable);
914
- });
915
- expect(getMeaningfulChildren(document)).toEqual(
916
- <html>
917
- <head>
918
- <link rel="stylesheet" href="foo" data-precedence="default" />
919
- </head>
920
- <body>foo</body>
921
- </html>,
922
- );
923
- });
973
+ expect(getMeaningfulChildren(document)).toEqual(
974
+ <html>
975
+ <head>
976
+ <link rel="stylesheet" href="one1" data-precedence="one" />
977
+ <link rel="stylesheet" href="one2" data-precedence="one" />
978
+ <style data-href="one3" data-precedence="one">
979
+ {css}
980
+ </style>
981
+ <link rel="stylesheet" href="one4" data-precedence="one" />
982
+ <style data-href="two1" data-precedence="two">
983
+ {css}
984
+ </style>
985
+ <link rel="stylesheet" href="two2" data-precedence="two" />
986
+ <style data-href="two3" data-precedence="two">
987
+ {css}
988
+ </style>
989
+ <style data-href="two4" data-precedence="two">
990
+ {css}
991
+ </style>
992
+ <link rel="stylesheet" href="three1" data-precedence="three" />
993
+ <style data-href="three2" data-precedence="three">
994
+ {css}
995
+ </style>
996
+ <link rel="stylesheet" href="three3" data-precedence="three" />
997
+ <link rel="stylesheet" href="three4" data-precedence="three" />
998
+ <style data-href="four1" data-precedence="four">
999
+ {css}
1000
+ </style>
1001
+ <style data-href="four2" data-precedence="four">
1002
+ {css}
1003
+ </style>
1004
+ <link rel="stylesheet" href="four3" data-precedence="four" />
1005
+ <style data-href="four4" data-precedence="four">
1006
+ {css}
1007
+ </style>
1008
+ <link rel="stylesheet" href="five1" data-precedence="five" />
1009
+ <style data-href="six1" data-precedence="six">
1010
+ {css}
1011
+ </style>
1012
+ <link rel="stylesheet" href="seven1" data-precedence="seven" />
1013
+ <style data-href="eight1" data-precedence="eight">
1014
+ {css}
1015
+ </style>
1016
+ <link rel="preload" href="one4" as="style" />
1017
+ <link rel="preload" href="three4" as="style" />
1018
+ <link rel="preload" href="seven1" as="style" />
1019
+ </head>
1020
+ <body>
1021
+ <link rel="preload" href="one2" as="style" />
1022
+ <link rel="preload" href="two2" as="style" />
1023
+ <link rel="preload" href="five1" as="style" />
1024
+ <link rel="preload" href="three3" as="style" />
1025
+ <link rel="preload" href="four3" as="style" />
1026
+ </body>
1027
+ </html>,
1028
+ );
1029
+ });
1030
925
- // @gate enableFloat
926
- it('creates a preload Resource when called during server rendering after first flush', async () => {
927
- function BlockedOn({text, children}) {
928
- readText(text);
929
- return children;
930
- }
931
- function Component() {
932
- ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
933
- return 'foo';
934
- }
935
- await actIntoEmptyDocument(() => {
936
- const {pipe} = renderToPipeableStream(
937
- <html>
938
- <head />
939
- <body>
940
- <Suspense fallback="loading...">
941
- <BlockedOn text="unblock">
942
- <Component />
943
- </BlockedOn>
944
- </Suspense>
945
- </body>
946
- </html>,
947
- );
948
- pipe(writable);
949
- });
950
- await act(() => {
951
- resolveText('unblock');
952
- });
953
- expect(getMeaningfulChildren(document)).toEqual(
1031
+ // @gate enableFloat
1032
+ it('client renders a boundary if a style Resource dependency fails to load', async () => {
1033
+ function App() {
1034
+ return (
1035
<html>
1036
<head />
1037
<body>
957
- foo
958
- <link rel="preload" as="style" href="foo" />
1038
+ <Suspense fallback="loading...">
1039
+ <BlockedOn value="unblock">
1040
+ <link rel="stylesheet" href="foo" precedence="arbitrary" />
1041
+ <link rel="stylesheet" href="bar" precedence="arbitrary" />
1042
+ Hello
1043
+ </BlockedOn>
1044
+ </Suspense>
1045
</body>
960
- </html>,
1046
+ </html>
1047
);
1048
+ }
1049
+ await actIntoEmptyDocument(() => {
1050
+ const {pipe} = renderToPipeableStream(<App />);
1051
+ pipe(writable);
1052
});
1053
964
- // @gate enableFloat
965
- it('inserts a style Resource into the document during render when called during client rendering', async () => {
966
- function Component() {
967
- ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
968
- return 'foo';
969
- }
970
- const root = ReactDOMClient.createRoot(container);
971
- root.render(<Component />);
972
- expect(Scheduler).toFlushWithoutYielding();
973
- expect(getMeaningfulChildren(document)).toEqual(
974
- <html>
975
- <head>
976
- <link rel="stylesheet" href="foo" data-precedence="foo" />
977
- </head>
978
- <body>
979
- <div id="container">foo</div>
980
- </body>
981
- </html>,
1054
+ await act(() => {
1055
+ resolveText('unblock');
1056
+ });
1057
+
1058
+ expect(getMeaningfulChildren(document)).toEqual(
1059
+ <html>
1060
+ <head>
1061
+ <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1062
+ <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1063
+ </head>
1064
+ <body>
1065
+ loading...
1066
+ <link rel="preload" href="foo" as="style" />
1067
+ <link rel="preload" href="bar" as="style" />
1068
+ </body>
1069
+ </html>,
1070
+ );
1071
+
1072
+ await act(() => {
1073
+ const barLink = document.querySelector(
1074
+ 'link[rel="stylesheet"][href="bar"]',
1075
);
1076
+ const event = document.createEvent('Events');
1077
+ event.initEvent('error', true, true);
1078
+ barLink.dispatchEvent(event);
1079
});
1080
985
- // @gate enableFloat
986
- it('inserts a preload resource into the document when called in an insertion effect, layout effect, or passive effect', async () => {
987
- function App() {
988
- React.useEffect(() => {
989
- ReactDOM.preinit('passive', {as: 'style', precedence: 'default'});
990
- }, []);
991
- React.useLayoutEffect(() => {
992
- ReactDOM.preinit('layout', {as: 'style', precedence: 'default'});
993
- });
994
- React.useInsertionEffect(() => {
995
- ReactDOM.preinit('insertion', {as: 'style', precedence: 'default'});
996
- });
997
- return 'foobar';
998
- }
999
- const root = ReactDOMClient.createRoot(container);
1000
- root.render(<App />);
1001
- expect(Scheduler).toFlushWithoutYielding();
1081
+ const boundaryTemplateInstance = document.getElementById('B:0');
1082
+ const suspenseInstance = boundaryTemplateInstance.previousSibling;
1083
1003
- expect(getMeaningfulChildren(document)).toEqual(
1084
+ expect(suspenseInstance.data).toEqual('$!');
1085
+ expect(boundaryTemplateInstance.dataset.dgst).toBe(
1086
+ 'Resource failed to load',
1087
+ );
1088
+
1089
+ expect(getMeaningfulChildren(document)).toEqual(
1090
+ <html>
1091
+ <head>
1092
+ <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1093
+ <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1094
+ </head>
1095
+ <body>
1096
+ loading...
1097
+ <link rel="preload" href="foo" as="style" />
1098
+ <link rel="preload" href="bar" as="style" />
1099
+ </body>
1100
+ </html>,
1101
+ );
1102
+
1103
+ const errors = [];
1104
+ ReactDOMClient.hydrateRoot(document, <App />, {
1105
+ onRecoverableError(err, errInfo) {
1106
+ errors.push(err.message);
1107
+ errors.push(err.digest);
1108
+ },
1109
+ });
1110
+ expect(Scheduler).toFlushWithoutYielding();
1111
+ expect(getMeaningfulChildren(document)).toEqual(
1112
+ <html>
1113
+ <head>
1114
+ <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1115
+ <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1116
+ </head>
1117
+ <body>
1118
+ <link rel="preload" href="foo" as="style" />
1119
+ <link rel="preload" href="bar" as="style" />
1120
+ Hello
1121
+ </body>
1122
+ </html>,
1123
+ );
1124
+ expect(errors).toEqual([
1125
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1126
+ 'Resource failed to load',
1127
+ ]);
1128
+ });
1129
+
1130
+ // @gate enableFloat
1131
+ it('treats stylesheet links with a precedence as a resource', async () => {
1132
+ await actIntoEmptyDocument(() => {
1133
+ const {pipe} = renderToPipeableStream(
1134
<html>
1005
- <head>
1006
- <link rel="preload" as="style" href="insertion" />
1007
- <link rel="preload" as="style" href="layout" />
1008
- <link rel="preload" as="style" href="passive" />
1009
- </head>
1135
+ <head />
1136
<body>
1011
- <div id="container">foobar</div>
1137
+ <link rel="stylesheet" href="foo" precedence="arbitrary" />
1138
+ Hello
1139
</body>
1140
</html>,
1141
);
1142
+ pipe(writable);
1143
});
1144
+ expect(getMeaningfulChildren(document)).toEqual(
1145
+ <html>
1146
+ <head>
1147
+ <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1148
+ </head>
1149
+ <body>Hello</body>
1150
+ </html>,
1151
+ );
1152
1017
- // @gate enableFloat
1018
- it('inserts a preload resource when called in module scope', async () => {
1019
- // The requirement that a root be created has to do with bootstrapping the dispatcher.
1020
- // We are intentionally avoiding setting it to the default via import due to cycles and
1021
- // we are trying to avoid doing a mutable initailation in module scope.
1022
- ReactDOM.preinit('foo', {as: 'style'});
1023
- ReactDOMClient.hydrateRoot(container, null);
1024
- ReactDOM.preinit('bar', {as: 'style'});
1025
- // We need to use global.document because preload falls back
1026
- // to the window.document global when no other documents have been used
1027
- // The way the JSDOM runtim is created for these tests the local document
1028
- // global does not point to the global.document
1029
- expect(getMeaningfulChildren(global.document)).toEqual(
1030
- <html>
1031
- <head>
1032
- <link rel="preload" as="style" href="bar" />
1033
- </head>
1034
- <body />
1035
- </html>,
1036
- );
1037
- });
1153
+ ReactDOMClient.hydrateRoot(
1154
+ document,
1155
+ <html>
1156
+ <head />
1157
+ <body>Hello</body>
1158
+ </html>,
1159
+ );
1160
+ expect(Scheduler).toFlushWithoutYielding();
1161
+ expect(getMeaningfulChildren(document)).toEqual(
1162
+ <html>
1163
+ <head>
1164
+ <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1165
+ </head>
1166
+ <body>Hello</body>
1167
+ </html>,
1168
+ );
1169
});
1170
1040
- describe('ReactDOM.preinit as script', () => {
1041
- // @gate enableFloat
1042
- it('can preinit a script', async () => {
1043
- function App({srcs}) {
1044
- srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'}));
1045
- return (
1046
- <html>
1047
- <head>
1048
- <title>title</title>
1049
- </head>
1050
- <body>foo</body>
1051
- </html>
1052
- );
1053
- }
1054
- await actIntoEmptyDocument(() => {
1055
- const {pipe} = renderToPipeableStream(
1056
- <App srcs={['server', 'shared']} />,
1057
- );
1058
- pipe(writable);
1059
- });
1060
- expect(getMeaningfulChildren(document)).toEqual(
1061
- <html>
1062
- <head>
1063
- <script src="server" async="" />
1064
- <script src="shared" async="" />
1065
- <title>title</title>
1066
- </head>
1067
- <body>foo</body>
1068
- </html>,
1171
+ // @gate enableFloat
1172
+ 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 () => {
1173
+ // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
1174
+ // because the link gets elided as a resource
1175
+ function AsyncTextWithResource({text, href, precedence}) {
1176
+ const value = readText(text);
1177
+ return (
1178
+ <>
1179
+ {value}
1180
+ <link rel="stylesheet" href={href} precedence={precedence} />
1181
+ </>
1182
);
1183
+ }
1184
1071
- ReactDOMClient.hydrateRoot(document, <App srcs={['client', 'shared']} />);
1072
- expect(Scheduler).toFlushWithoutYielding();
1073
- expect(getMeaningfulChildren(document)).toEqual(
1185
+ await actIntoEmptyDocument(() => {
1186
+ const {pipe} = renderToPipeableStream(
1187
<html>
1075
- <head>
1076
- <script src="server" async="" />
1077
- <script src="shared" async="" />
1078
- <title>title</title>
1079
- <script src="client" async="" />
1080
- </head>
1081
- <body>foo</body>
1188
+ <head />
1189
+ <body>
1190
+ <AsyncTextWithResource text="foo" href="foo" precedence="one" />
1191
+ <AsyncTextWithResource text="bar" href="bar" precedence="two" />
1192
+ <AsyncTextWithResource text="baz" href="baz" precedence="three" />
1193
+ </body>
1194
</html>,
1195
);
1196
+ pipe(writable);
1197
+ resolveText('foo');
1198
+ resolveText('bar');
1199
+ resolveText('baz');
1200
});
1201
+
1202
+ expect(getMeaningfulChildren(document)).toEqual(
1203
+ <html>
1204
+ <head>
1205
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1206
+ <link rel="stylesheet" href="bar" data-precedence="two" />
1207
+ <link rel="stylesheet" href="baz" data-precedence="three" />
1208
+ </head>
1209
+ <body>
1210
+ {'foo'}
1211
+ {'bar'}
1212
+ {'baz'}
1213
+ </body>
1214
+ </html>,
1215
+ );
1216
});
1217
1087
- describe('document encapsulation', () => {
1088
- // @gate enableFloat
1089
- it('can support styles inside portals to a shadowRoot', async () => {
1090
- const shadow = document.body.attachShadow({mode: 'open'});
1091
- const root = ReactDOMClient.createRoot(container);
1092
- root.render(
1093
- <>
1094
- <link rel="stylesheet" href="foo" precedence="default" />
1095
- {ReactDOM.createPortal(
1096
- <div>
1097
- <link
1098
- rel="stylesheet"
1099
- href="foo"
1100
- data-extra-prop="foo"
1101
- precedence="different"
1102
- />
1103
- shadow
1104
- </div>,
1105
- shadow,
1106
- )}
1107
- container
1108
- </>,
1109
- );
1110
- expect(Scheduler).toFlushWithoutYielding();
1111
- expect(getMeaningfulChildren(document)).toEqual(
1218
+ // @gate enableFloat
1219
+ it('hoists late stylesheets the correct precedence', async () => {
1220
+ function PresetPrecedence() {
1221
+ ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
1222
+ }
1223
+ await actIntoEmptyDocument(() => {
1224
+ const {pipe} = renderToPipeableStream(
1225
<html>
1113
- <head>
1114
- <link rel="stylesheet" href="foo" data-precedence="default" />
1115
- <link rel="preload" href="foo" as="style" />
1116
- </head>
1226
+ <head />
1227
<body>
1118
- <div id="container">container</div>
1119
- </body>
1120
- </html>,
1121
- );
1122
- expect(getMeaningfulChildren(shadow)).toEqual([
1123
- <link
1124
- rel="stylesheet"
1125
- href="foo"
1126
- data-precedence="different"
1127
- data-extra-prop="foo"
1128
- />,
1129
- <div>shadow</div>,
1130
- ]);
1131
- });
1132
- // @gate enableFloat
1133
- it('can support styles inside portals to an element in shadowRoots', async () => {
1134
- const template = document.createElement('template');
1135
- template.innerHTML =
1136
- "<div><div id='shadowcontainer1'></div><div id='shadowcontainer2'></div></div>";
1137
- const shadow = document.body.attachShadow({mode: 'open'});
1138
- shadow.appendChild(template.content);
1139
-
1140
- const shadowContainer1 = shadow.getElementById('shadowcontainer1');
1141
- const shadowContainer2 = shadow.getElementById('shadowcontainer2');
1142
- const root = ReactDOMClient.createRoot(container);
1143
- root.render(
1144
- <>
1145
- <link rel="stylesheet" href="foo" precedence="default" />
1146
- {ReactDOM.createPortal(
1228
+ <link rel="stylesheet" href="initial" precedence="one" />
1229
+ <PresetPrecedence />
1230
<div>
1148
- <link rel="stylesheet" href="foo" precedence="one" />
1149
- <link rel="stylesheet" href="bar" precedence="two" />1
1150
- </div>,
1151
- shadow,
1152
- )}
1153
- {ReactDOM.createPortal(
1231
+ <Suspense fallback="loading foo bar...">
1232
+ <div>foo</div>
1233
+ <link rel="stylesheet" href="foo" precedence="one" />
1234
+ <BlockedOn value="bar">
1235
+ <div>bar</div>
1236
+ <link rel="stylesheet" href="bar" precedence="default" />
1237
+ </BlockedOn>
1238
+ </Suspense>
1239
+ </div>
1240
<div>
1155
- <link rel="stylesheet" href="foo" precedence="one" />
1156
- <link rel="stylesheet" href="baz" precedence="one" />2
1157
- </div>,
1158
- shadowContainer1,
1159
- )}
1160
- {ReactDOM.createPortal(
1241
+ <Suspense fallback="loading bar baz qux...">
1242
+ <BlockedOn value="bar">
1243
+ <div>bar</div>
1244
+ <link rel="stylesheet" href="bar" precedence="default" />
1245
+ </BlockedOn>
1246
+ <BlockedOn value="baz">
1247
+ <div>baz</div>
1248
+ <link rel="stylesheet" href="baz" precedence="two" />
1249
+ </BlockedOn>
1250
+ <BlockedOn value="qux">
1251
+ <div>qux</div>
1252
+ <link rel="stylesheet" href="qux" precedence="one" />
1253
+ </BlockedOn>
1254
+ </Suspense>
1255
+ </div>
1256
<div>
1162
- <link rel="stylesheet" href="bar" precedence="two" />
1163
- <link rel="stylesheet" href="qux" precedence="three" />3
1164
- </div>,
1165
- shadowContainer2,
1166
- )}
1167
- container
1168
- </>,
1169
- );
1170
- expect(Scheduler).toFlushWithoutYielding();
1171
- expect(getMeaningfulChildren(document)).toEqual(
1172
- <html>
1173
- <head>
1174
- <link rel="stylesheet" href="foo" data-precedence="default" />
1175
- <link rel="preload" href="foo" as="style" />
1176
- <link rel="preload" href="bar" as="style" />
1177
- <link rel="preload" href="baz" as="style" />
1178
- <link rel="preload" href="qux" as="style" />
1179
- </head>
1180
- <body>
1181
- <div id="container">container</div>
1257
+ <Suspense fallback="loading bar baz qux...">
1258
+ <BlockedOn value="unblock">
1259
+ <BlockedOn value="bar">
1260
+ <div>bar</div>
1261
+ <link rel="stylesheet" href="bar" precedence="default" />
1262
+ </BlockedOn>
1263
+ <BlockedOn value="baz">
1264
+ <div>baz</div>
1265
+ <link rel="stylesheet" href="baz" precedence="two" />
1266
+ </BlockedOn>
1267
+ <BlockedOn value="qux">
1268
+ <div>qux</div>
1269
+ <link rel="stylesheet" href="qux" precedence="one" />
1270
+ </BlockedOn>
1271
+ </BlockedOn>
1272
+ </Suspense>
1273
+ </div>
1274
</body>
1275
</html>,
1276
);
1185
- expect(getMeaningfulChildren(shadow)).toEqual([
1186
- <link rel="stylesheet" href="foo" data-precedence="one" />,
1187
- <link rel="stylesheet" href="baz" data-precedence="one" />,
1188
- <link rel="stylesheet" href="bar" data-precedence="two" />,
1189
- <link rel="stylesheet" href="qux" data-precedence="three" />,
1190
- <div>
1191
- <div id="shadowcontainer1">
1192
- <div>2</div>
1193
- </div>
1194
- <div id="shadowcontainer2">
1195
- <div>3</div>
1196
- </div>
1197
- </div>,
1198
- <div>1</div>,
1199
- ]);
1277
+ pipe(writable);
1278
});
1201
- });
1202
-
1203
- describe('head resources', () => {
1204
- // @gate enableFloat
1205
- it('supports preconnects, prefetc-dns, and arbitrary other link types', async () => {
1206
- await actIntoEmptyDocument(() => {
1207
- const {pipe} = renderToPipeableStream(
1208
- <html>
1209
- <head />
1210
- <body>
1211
- <div>hello world</div>
1212
- </body>
1213
- <link rel="foo" href="bar" />
1214
- <link rel="preload" href="bar" />
1215
- <link rel="preload" href="bar" as="style" />
1216
- <link rel="stylesheet" href="bar" precedence="default" />
1217
- <link rel="preconnect" href="bar" />
1218
- <link rel="dns-prefetch" href="bar" />
1219
- <link rel="icon" href="bar" />
1220
- <link rel="icon" href="bar" sizes="1x1" />
1221
- <link rel="icon" href="bar" media="foo" />
1222
- <link rel="shortcut icon" href="bar" />
1223
- <link rel="apple-touch-icon" href="bar" />
1224
- </html>,
1225
- );
1226
- pipe(writable);
1227
- });
1228
- // "preconnect" and "dns-prefetch" get hoisted to the front.
1229
- // All other generic links (not styles, or typed preloads)
1230
- // get emitted after styles and other higher priority Resources
1231
- // Sizes and Media are part of generic link keys
1232
- expect(getMeaningfulChildren(document)).toEqual(
1233
- <html>
1234
- <head>
1235
- <link rel="preconnect" href="bar" />
1236
- <link rel="dns-prefetch" href="bar" />
1237
- <link rel="stylesheet" href="bar" data-precedence="default" />
1238
- <link rel="foo" href="bar" />
1239
- <link rel="preload" href="bar" />
1240
- <link rel="icon" href="bar" />
1241
- <link rel="icon" href="bar" sizes="1x1" />
1242
- <link rel="icon" href="bar" media="foo" />
1243
- <link rel="shortcut icon" href="bar" />
1244
- <link rel="apple-touch-icon" href="bar" />
1245
- </head>
1246
- <body>
1247
- <div>hello world</div>
1248
- </body>
1249
- </html>,
1250
- );
1279
1252
- const root = ReactDOMClient.hydrateRoot(
1253
- document,
1254
- <html>
1255
- <head />
1256
- <body>
1257
- <div>hello world</div>
1258
- </body>
1259
- <link rel="foo" href="bar" />
1260
- <link rel="preload" href="bar" />
1261
- <link rel="preload" href="bar" as="style" />
1262
- <link rel="stylesheet" href="bar" precedence="default" />
1263
- <link rel="preconnect" href="bar" />
1264
- <link rel="dns-prefetch" href="bar" />
1265
- <link rel="icon" href="bar" />
1266
- <link rel="icon" href="bar" sizes="1x1" />
1267
- <link rel="icon" href="bar" media="foo" />
1268
- <link rel="shortcut icon" href="bar" />
1269
- <link rel="apple-touch-icon" href="bar" />
1270
- </html>,
1271
- );
1272
- expect(Scheduler).toFlushWithoutYielding();
1273
- expect(getMeaningfulChildren(document)).toEqual(
1274
- <html>
1275
- <head>
1276
- <link rel="preconnect" href="bar" />
1277
- <link rel="dns-prefetch" href="bar" />
1278
- <link rel="stylesheet" href="bar" data-precedence="default" />
1279
- <link rel="foo" href="bar" />
1280
- <link rel="preload" href="bar" />
1281
- <link rel="icon" href="bar" />
1282
- <link rel="icon" href="bar" sizes="1x1" />
1283
- <link rel="icon" href="bar" media="foo" />
1284
- <link rel="shortcut icon" href="bar" />
1285
- <link rel="apple-touch-icon" href="bar" />
1286
- </head>
1287
- <body>
1288
- <div>hello world</div>
1289
- </body>
1290
- </html>,
1291
- );
1292
-
1293
- root.render(
1294
- <html>
1295
- <head />
1296
- <body>
1297
- <div>hello world</div>
1298
- </body>
1299
- </html>,
1300
- );
1301
- expect(Scheduler).toFlushWithoutYielding();
1302
- expect(getMeaningfulChildren(document)).toEqual(
1303
- <html>
1304
- <head>
1305
- <link rel="stylesheet" href="bar" data-precedence="default" />
1306
- <link rel="preload" href="bar" />
1307
- </head>
1308
- <body>
1309
- <div>hello world</div>
1310
- </body>
1311
- </html>,
1312
- );
1313
- });
1314
-
1315
- // @gate enableFloat
1316
- it('can render <base> as a Resource', async () => {
1317
- await actIntoEmptyDocument(() => {
1318
- const {pipe} = renderToPipeableStream(
1319
- <html>
1320
- <head />
1321
- <body>
1322
- <base target="_blank" />
1323
- <base href="foo" />
1324
- <base target="_self" href="bar" />
1325
- <div>hello world</div>
1326
- </body>
1327
- </html>,
1328
- );
1329
- pipe(writable);
1330
- });
1331
- expect(getMeaningfulChildren(document)).toEqual(
1332
- <html>
1333
- <head>
1334
- <base target="_blank" />
1335
- <base href="foo" />
1336
- <base target="_self" href="bar" />
1337
- </head>
1338
- <body>
1339
- <div>hello world</div>
1340
- </body>
1341
- </html>,
1342
- );
1343
-
1344
- ReactDOMClient.hydrateRoot(
1345
- document,
1346
- <html>
1347
- <head />
1348
- <body>
1349
- <base target="_blank" />
1350
- <base href="foo" />
1351
- <base target="_self" href="bar" />
1352
- <base target="_top" href="baz" />
1353
- <div>hello world</div>
1354
- </body>
1355
- </html>,
1356
- );
1357
- expect(Scheduler).toFlushWithoutYielding();
1358
- expect(getMeaningfulChildren(document)).toEqual(
1359
- <html>
1360
- <head>
1361
- <base target="_top" href="baz" />
1362
- <base target="_blank" />
1363
- <base href="foo" />
1364
- <base target="_self" href="bar" />
1365
- </head>
1366
- <body>
1367
- <div>hello world</div>
1368
- </body>
1369
- </html>,
1370
- );
1371
- });
1372
-
1373
- // @gate enableFloat
1374
- it('can render icons and apple-touch-icons as Resources', async () => {
1375
- await actIntoEmptyDocument(() => {
1376
- const {pipe} = renderToPipeableStream(
1377
- <>
1378
- <html>
1379
- <head />
1380
- <body>
1381
- <link rel="icon" href="foo" />
1382
- <div>hello world</div>
1383
- </body>
1384
- </html>
1385
- <link rel="apple-touch-icon" href="foo" />
1386
- </>,
1387
- );
1388
- pipe(writable);
1389
- });
1390
- expect(getMeaningfulChildren(document)).toEqual(
1391
- <html>
1392
- <head>
1393
- <link rel="icon" href="foo" />
1394
- <link rel="apple-touch-icon" href="foo" />
1395
- </head>
1396
- <body>
1397
- <div>hello world</div>
1398
- </body>
1399
- </html>,
1400
- );
1280
+ expect(getMeaningfulChildren(document)).toEqual(
1281
+ <html>
1282
+ <head>
1283
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1284
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1285
+ <link rel="preload" href="foo" as="style" />
1286
+ </head>
1287
+ <body>
1288
+ <div>loading foo bar...</div>
1289
+ <div>loading bar baz qux...</div>
1290
+ <div>loading bar baz qux...</div>
1291
+ </body>
1292
+ </html>,
1293
+ );
1294
1402
- ReactDOMClient.hydrateRoot(
1403
- document,
1404
- <html>
1405
- <link rel="apple-touch-icon" href="foo" />
1406
- <head />
1407
- <body>
1408
- <link rel="icon" href="foo" />
1409
- <div>hello world</div>
1410
- </body>
1411
- </html>,
1412
- );
1413
- expect(Scheduler).toFlushWithoutYielding();
1414
- expect(getMeaningfulChildren(document)).toEqual(
1415
- <html>
1416
- <head>
1417
- <link rel="icon" href="foo" />
1418
- <link rel="apple-touch-icon" href="foo" />
1419
- </head>
1420
- <body>
1421
- <div>hello world</div>
1422
- </body>
1423
- </html>,
1424
- );
1295
+ await act(() => {
1296
+ resolveText('foo');
1297
+ resolveText('bar');
1298
});
1299
1427
- // @gate enableFloat
1428
- it('can hydrate the right instances for deeply nested structured metas', async () => {
1429
- await actIntoEmptyDocument(() => {
1430
- const {pipe} = renderToPipeableStream(
1431
- <>
1432
- <html>
1433
- <head />
1434
- <body>
1435
- <div>hello world</div>
1436
- </body>
1437
- </html>
1438
- <meta property="og:foo" content="one" />
1439
- <meta property="og:foo:bar" content="bar" />
1440
- <meta property="og:foo:bar:baz" content="baz" />
1441
- <meta property="og:foo" content="two" />
1442
- <meta property="og:foo:bar" content="bar" />
1443
- <meta property="og:foo:bar:baz" content="baz" />
1444
- </>,
1445
- );
1446
- pipe(writable);
1447
- });
1448
- expect(getMeaningfulChildren(document)).toEqual(
1449
- <html>
1450
- <head>
1451
- <meta property="og:foo" content="one" />
1452
- <meta property="og:foo:bar" content="bar" />
1453
- <meta property="og:foo:bar:baz" content="baz" />
1454
- <meta property="og:foo" content="two" />
1455
- <meta property="og:foo:bar" content="bar" />
1456
- <meta property="og:foo:bar:baz" content="baz" />
1457
- </head>
1458
- <body>
1459
- <div>hello world</div>
1460
- </body>
1461
- </html>,
1462
- );
1463
-
1464
- const root = ReactDOMClient.hydrateRoot(
1465
- document,
1466
- <>
1467
- <html>
1468
- <head />
1469
- <body>
1470
- <div>hello world</div>
1471
- </body>
1472
- </html>
1473
- <meta property="og:foo" content="one" />
1474
- <meta property="og:foo:bar" content="bar" />
1475
- <meta property="og:foo:bar:baz" content="baz" />
1476
- <meta property="og:foo" content="two" />
1477
- <meta property="og:foo:bar" content="bar" />
1478
- <meta property="og:foo:bar:baz" content="baz" />
1479
- </>,
1480
- );
1481
- expect(Scheduler).toFlushWithoutYielding();
1482
- expect(getMeaningfulChildren(document)).toEqual(
1483
- <html>
1484
- <head>
1485
- <meta property="og:foo" content="one" />
1486
- <meta property="og:foo:bar" content="bar" />
1487
- <meta property="og:foo:bar:baz" content="baz" />
1488
- <meta property="og:foo" content="two" />
1489
- <meta property="og:foo:bar" content="bar" />
1490
- <meta property="og:foo:bar:baz" content="baz" />
1491
- </head>
1492
- <body>
1493
- <div>hello world</div>
1494
- </body>
1495
- </html>,
1496
- );
1300
+ expect(getMeaningfulChildren(document)).toEqual(
1301
+ <html>
1302
+ <head>
1303
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1304
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1305
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1306
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1307
+ <link rel="preload" href="foo" as="style" />
1308
+ </head>
1309
+ <body>
1310
+ <div>loading foo bar...</div>
1311
+ <div>loading bar baz qux...</div>
1312
+ <div>loading bar baz qux...</div>
1313
+ <link rel="preload" href="bar" as="style" />
1314
+ </body>
1315
+ </html>,
1316
+ );
1317
1498
- root.render(
1499
- <>
1500
- <html>
1501
- <head />
1502
- <body>
1503
- <div>hello world</div>
1504
- </body>
1505
- </html>
1506
- </>,
1507
- );
1508
- expect(Scheduler).toFlushWithoutYielding();
1509
- expect(getMeaningfulChildren(document)).toEqual(
1510
- <html>
1511
- <head />
1512
- <body>
1513
- <div>hello world</div>
1514
- </body>
1515
- </html>,
1516
- );
1318
+ await act(() => {
1319
+ const link = document.querySelector('link[rel="stylesheet"][href="foo"]');
1320
+ const event = document.createEvent('Events');
1321
+ event.initEvent('load', true, true);
1322
+ link.dispatchEvent(event);
1323
});
1324
1519
- // @gate enableFloat
1520
- it('can insert meta tags in the expected location', async () => {
1521
- await actIntoEmptyDocument(() => {
1522
- const {pipe} = renderToPipeableStream(
1523
- <>
1524
- <html>
1525
- <head />
1526
- <body>
1527
- <div>hello world</div>
1528
- </body>
1529
- </html>
1530
- <meta charSet="utf-8" />
1531
- <meta name="google-site-verification" content="somehash1" />
1532
- <meta name="google-site-verification" content="somehash2" />
1533
- <meta
1534
- name="description"
1535
- property="og:description"
1536
- content="my site"
1537
- />
1538
- <meta property="og:image" content="foo" />
1539
- <meta property="og:image:width" content="100" />
1540
- <meta httpEquiv="refresh" content="dont actually" />
1541
- <meta property="og:image:height" content="100" />
1542
- <meta property="og:image" content="bar" />
1543
- <meta property="og:image:width" content="100" />
1544
- <meta itemProp="someprop" content="somevalue" />
1545
- <meta property="og:image:height" content="100" />
1546
- <meta property="og:description:foo" content="foo" />
1547
- </>,
1548
- );
1549
- pipe(writable);
1550
- });
1551
- expect(getMeaningfulChildren(document)).toEqual(
1552
- <html>
1553
- <head>
1554
- <meta charset="utf-8" />
1555
- <meta name="google-site-verification" content="somehash1" />
1556
- <meta name="google-site-verification" content="somehash2" />
1557
- <meta
1558
- name="description"
1559
- property="og:description"
1560
- content="my site"
1561
- />
1562
- <meta property="og:image" content="foo" />
1563
- <meta property="og:image:width" content="100" />
1564
- <meta http-equiv="refresh" content="dont actually" />
1565
- <meta property="og:image:height" content="100" />
1566
- <meta property="og:image" content="bar" />
1567
- <meta property="og:image:width" content="100" />
1568
- <meta itemprop="someprop" content="somevalue" />
1569
- <meta property="og:image:height" content="100" />
1570
- <meta property="og:description:foo" content="foo" />
1571
- </head>
1572
- <body>
1573
- <div>hello world</div>
1574
- </body>
1575
- </html>,
1576
- );
1577
-
1578
- const root = ReactDOMClient.hydrateRoot(
1579
- document,
1580
- <html>
1581
- <head>
1582
- <meta property="og:image" content="bar" />
1583
- <meta property="og:image:width" content="100" />
1584
- <meta property="og:image:height" content="100" />
1585
- </head>
1586
- <body>
1587
- <div>hello world</div>
1588
- </body>
1589
- </html>,
1590
- );
1591
- expect(Scheduler).toFlushWithoutYielding();
1592
- expect(getMeaningfulChildren(document)).toEqual(
1593
- <html>
1594
- <head>
1595
- <meta charset="utf-8" />
1596
- <meta name="google-site-verification" content="somehash1" />
1597
- <meta name="google-site-verification" content="somehash2" />
1598
- <meta
1599
- name="description"
1600
- property="og:description"
1601
- content="my site"
1602
- />
1603
- <meta property="og:image" content="foo" />
1604
- <meta property="og:image:width" content="100" />
1605
- <meta http-equiv="refresh" content="dont actually" />
1606
- <meta property="og:image:height" content="100" />
1607
- <meta property="og:image" content="bar" />
1608
- <meta property="og:image:width" content="100" />
1609
- <meta itemprop="someprop" content="somevalue" />
1610
- <meta property="og:image:height" content="100" />
1611
- <meta property="og:description:foo" content="foo" />
1612
- </head>
1613
- <body>
1614
- <div>hello world</div>
1615
- </body>
1616
- </html>,
1617
- );
1325
+ expect(getMeaningfulChildren(document)).toEqual(
1326
+ <html>
1327
+ <head>
1328
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1329
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1330
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1331
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1332
+ <link rel="preload" href="foo" as="style" />
1333
+ </head>
1334
+ <body>
1335
+ <div>loading foo bar...</div>
1336
+ <div>loading bar baz qux...</div>
1337
+ <div>loading bar baz qux...</div>
1338
+ <link rel="preload" href="bar" as="style" />
1339
+ </body>
1340
+ </html>,
1341
+ );
1342
1619
- root.render(
1620
- <html>
1621
- <head>
1622
- <meta property="og:image" content="bar" />
1623
- <meta property="og:image:width" content="100" />
1624
- <meta property="og:image:height" content="100" />
1625
- <meta property="og:description" content="my site" />
1626
- <meta
1627
- itemProp="description bar"
1628
- property="og:description:bar"
1629
- content="bar"
1630
- />
1631
- </head>
1632
- <body>
1633
- <div>hello world</div>
1634
- </body>
1635
- </html>,
1636
- );
1637
- expect(Scheduler).toFlushWithoutYielding();
1638
- expect(getMeaningfulChildren(document)).toEqual(
1639
- <html>
1640
- <head>
1641
- <meta charset="utf-8" />
1642
- <meta name="google-site-verification" content="somehash1" />
1643
- <meta name="google-site-verification" content="somehash2" />
1644
- <meta
1645
- name="description"
1646
- property="og:description"
1647
- content="my site"
1648
- />
1649
- <meta
1650
- itemprop="description bar"
1651
- property="og:description:bar"
1652
- content="bar"
1653
- />
1654
- <meta property="og:image" content="foo" />
1655
- <meta property="og:image:width" content="100" />
1656
- <meta http-equiv="refresh" content="dont actually" />
1657
- <meta property="og:image:height" content="100" />
1658
- <meta property="og:image" content="bar" />
1659
- <meta property="og:image:width" content="100" />
1660
- <meta itemprop="someprop" content="somevalue" />
1661
- <meta property="og:image:height" content="100" />
1662
- <meta property="og:description:foo" content="foo" />
1663
- </head>
1664
- <body>
1665
- <div>hello world</div>
1666
- </body>
1667
- </html>,
1668
- );
1343
+ await act(() => {
1344
+ const link = document.querySelector('link[rel="stylesheet"][href="bar"]');
1345
+ const event = document.createEvent('Events');
1346
+ event.initEvent('load', true, true);
1347
+ link.dispatchEvent(event);
1348
});
1349
1671
- // @gate enableFloat
1672
- it('can render meta tags with og properties with structured data', async () => {
1673
- await actIntoEmptyDocument(() => {
1674
- const {pipe} = renderToPipeableStream(
1675
- <>
1676
- <html>
1677
- <head />
1678
- <body>
1679
- <div>hello world</div>
1680
- </body>
1681
- </html>
1682
- <meta property="og:image" content="foo" />
1683
- <meta property="og:image:width" content="100" />
1684
- <meta property="og:image:height" content="100" />
1685
- <meta property="og:image" content="bar" />
1686
- <meta property="og:image:width" content="100" />
1687
- <meta property="og:image:height" content="100" />
1688
- </>,
1689
- );
1690
- pipe(writable);
1691
- });
1692
- expect(getMeaningfulChildren(document)).toEqual(
1693
- <html>
1694
- <head>
1695
- <meta property="og:image" content="foo" />
1696
- <meta property="og:image:width" content="100" />
1697
- <meta property="og:image:height" content="100" />
1698
- <meta property="og:image" content="bar" />
1699
- <meta property="og:image:width" content="100" />
1700
- <meta property="og:image:height" content="100" />
1701
- </head>
1702
- <body>
1703
- <div>hello world</div>
1704
- </body>
1705
- </html>,
1706
- );
1350
+ expect(getMeaningfulChildren(document)).toEqual(
1351
+ <html>
1352
+ <head>
1353
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1354
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1355
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1356
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1357
+ <link rel="preload" href="foo" as="style" />
1358
+ </head>
1359
+ <body>
1360
+ <div>
1361
+ <div>foo</div>
1362
+ <div>bar</div>
1363
+ </div>
1364
+ <div>loading bar baz qux...</div>
1365
+ <div>loading bar baz qux...</div>
1366
+ <link rel="preload" href="bar" as="style" />
1367
+ </body>
1368
+ </html>,
1369
+ );
1370
1708
- const root = ReactDOMClient.hydrateRoot(
1709
- document,
1710
- <html>
1711
- <head />
1712
- <body>
1713
- <meta property="og:image" content="foo" />
1714
- <meta property="og:image:width" content="100" />
1715
- <meta property="og:image:height" content="100" />
1716
- <meta property="og:image" content="bar" />
1717
- <meta property="og:image:width" content="100" />
1718
- <meta property="og:image:height" content="100" />
1719
- <div>hello world</div>
1720
- </body>
1721
- </html>,
1722
- );
1723
- expect(Scheduler).toFlushWithoutYielding();
1724
- expect(getMeaningfulChildren(document)).toEqual(
1725
- <html>
1726
- <head>
1727
- <meta property="og:image" content="foo" />
1728
- <meta property="og:image:width" content="100" />
1729
- <meta property="og:image:height" content="100" />
1730
- <meta property="og:image" content="bar" />
1731
- <meta property="og:image:width" content="100" />
1732
- <meta property="og:image:height" content="100" />
1733
- </head>
1734
- <body>
1735
- <div>hello world</div>
1736
- </body>
1737
- </html>,
1738
- );
1371
+ await act(() => {
1372
+ resolveText('baz');
1373
+ });
1374
1740
- root.render(
1741
- <html>
1742
- <head />
1743
- <body>
1744
- <meta property="og:image" content="foo" />
1745
- <meta property="og:image:width" content="100" />
1746
- <meta property="og:image:height" content="100" />
1747
- <meta property="og:image" content="bar" />
1748
- <meta property="og:image:height" content="100" />
1749
- <div>hello world</div>
1750
- </body>
1751
- </html>,
1752
- );
1753
- expect(Scheduler).toFlushWithoutYielding();
1754
- expect(getMeaningfulChildren(document)).toEqual(
1755
- <html>
1756
- <head>
1757
- <meta property="og:image" content="foo" />
1758
- <meta property="og:image:width" content="100" />
1759
- <meta property="og:image:height" content="100" />
1760
- <meta property="og:image" content="bar" />
1761
- <meta property="og:image:height" content="100" />
1762
- </head>
1763
- <body>
1764
- <div>hello world</div>
1765
- </body>
1766
- </html>,
1767
- );
1375
+ expect(getMeaningfulChildren(document)).toEqual(
1376
+ <html>
1377
+ <head>
1378
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1379
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1380
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1381
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1382
+ <link rel="preload" href="foo" as="style" />
1383
+ </head>
1384
+ <body>
1385
+ <div>
1386
+ <div>foo</div>
1387
+ <div>bar</div>
1388
+ </div>
1389
+ <div>loading bar baz qux...</div>
1390
+ <div>loading bar baz qux...</div>
1391
+ <link rel="preload" as="style" href="bar" />
1392
+ <link rel="preload" as="style" href="baz" />
1393
+ </body>
1394
+ </html>,
1395
+ );
1396
1769
- root.render(
1770
- <html>
1771
- <head />
1772
- <body>
1773
- <meta property="og:image" content="foo" />
1774
- <meta property="og:image:width" content="100" />
1775
- <meta property="og:image:height" content="100" />
1776
- <meta property="og:image:foo" content="foo" />
1777
- <meta property="og:image" content="bar" />
1778
- <meta property="og:image:height" content="100" />
1779
- <div>hello world</div>
1780
- </body>
1781
- </html>,
1782
- );
1783
- expect(Scheduler).toFlushWithoutYielding();
1784
- expect(getMeaningfulChildren(document)).toEqual(
1785
- <html>
1786
- <head>
1787
- <meta property="og:image" content="foo" />
1788
- <meta property="og:image:foo" content="foo" />
1789
- <meta property="og:image:width" content="100" />
1790
- <meta property="og:image:height" content="100" />
1791
- <meta property="og:image" content="bar" />
1792
- <meta property="og:image:height" content="100" />
1793
- </head>
1794
- <body>
1795
- <div>hello world</div>
1796
- </body>
1797
- </html>,
1798
- );
1397
+ await act(() => {
1398
+ resolveText('qux');
1399
+ });
1400
1800
- root.render(
1801
- <html>
1802
- <head />
1803
- <body>
1804
- <meta property="og:image" content="foo" />
1805
- <meta property="og:image:width" content="100" />
1806
- <meta property="og:image:width:bar" content="bar" />
1807
- <meta property="og:image:height" content="100" />
1808
- <meta property="og:image:foo" content="foo" />
1809
- <meta property="og:image" content="bar" />
1810
- <meta property="og:image:height" content="100" />
1811
- <div>hello world</div>
1812
- </body>
1813
- </html>,
1401
+ expect(getMeaningfulChildren(document)).toEqual(
1402
+ <html>
1403
+ <head>
1404
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1405
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1406
+ <link rel="stylesheet" href="qux" data-precedence="one" />
1407
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1408
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1409
+ <link rel="stylesheet" href="baz" data-precedence="two" />
1410
+ <link rel="preload" href="foo" as="style" />
1411
+ </head>
1412
+ <body>
1413
+ <div>
1414
+ <div>foo</div>
1415
+ <div>bar</div>
1416
+ </div>
1417
+ <div>loading bar baz qux...</div>
1418
+ <div>loading bar baz qux...</div>
1419
+ <link rel="preload" as="style" href="bar" />
1420
+ <link rel="preload" as="style" href="baz" />
1421
+ <link rel="preload" as="style" href="qux" />
1422
+ </body>
1423
+ </html>,
1424
+ );
1425
+
1426
+ await act(() => {
1427
+ const bazlink = document.querySelector(
1428
+ 'link[rel="stylesheet"][href="baz"]',
1429
);
1815
- expect(Scheduler).toFlushWithoutYielding();
1816
- expect(getMeaningfulChildren(document)).toEqual(
1817
- <html>
1818
- <head>
1819
- <meta property="og:image" content="foo" />
1820
- <meta property="og:image:foo" content="foo" />
1821
- <meta property="og:image:height" content="100" />
1822
- <meta property="og:image:width" content="100" />
1823
- <meta property="og:image:width:bar" content="bar" />
1824
- <meta property="og:image" content="bar" />
1825
- <meta property="og:image:height" content="100" />
1826
- </head>
1827
- <body>
1828
- <div>hello world</div>
1829
- </body>
1830
- </html>,
1430
+ const quxlink = document.querySelector(
1431
+ 'link[rel="stylesheet"][href="qux"]',
1432
);
1433
+ const presetLink = document.querySelector(
1434
+ 'link[rel="stylesheet"][href="preset"]',
1435
+ );
1436
+ const event = document.createEvent('Events');
1437
+ event.initEvent('load', true, true);
1438
+ bazlink.dispatchEvent(event);
1439
+ quxlink.dispatchEvent(event);
1440
+ presetLink.dispatchEvent(event);
1441
});
1442
1834
- // @gate enableFloat
1835
- it('can render meta tags as resources', async () => {
1836
- await actIntoEmptyDocument(() => {
1837
- const {pipe} = renderToPipeableStream(
1838
- <>
1839
- <html>
1840
- <head />
1841
- <body>
1842
- <div>hello world</div>
1843
- </body>
1844
- </html>
1845
- <meta name="robots" content="noindex" />
1846
- <meta httpEquiv="content-security-policy" content="foo" />
1847
- <meta itemProp="description" content="desc" />
1848
- <meta property="description" content="desc2" />
1849
- <meta charSet="utf-8" />
1850
- </>,
1851
- );
1852
- pipe(writable);
1853
- });
1854
- expect(getMeaningfulChildren(document)).toEqual(
1855
- <html>
1856
- <head>
1857
- <meta charset="utf-8" />
1858
- <meta name="robots" content="noindex" />
1859
- <meta http-equiv="content-security-policy" content="foo" />
1860
- <meta itemprop="description" content="desc" />
1861
- <meta property="description" content="desc2" />
1862
- </head>
1863
- <body>
1864
- <div>hello world</div>
1865
- </body>
1866
- </html>,
1867
- );
1443
+ expect(getMeaningfulChildren(document)).toEqual(
1444
+ <html>
1445
+ <head>
1446
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1447
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1448
+ <link rel="stylesheet" href="qux" data-precedence="one" />
1449
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1450
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1451
+ <link rel="stylesheet" href="baz" data-precedence="two" />
1452
+ <link rel="preload" href="foo" as="style" />
1453
+ </head>
1454
+ <body>
1455
+ <div>
1456
+ <div>foo</div>
1457
+ <div>bar</div>
1458
+ </div>
1459
+ <div>
1460
+ <div>bar</div>
1461
+ <div>baz</div>
1462
+ <div>qux</div>
1463
+ </div>
1464
+ <div>loading bar baz qux...</div>
1465
+ <link rel="preload" as="style" href="bar" />
1466
+ <link rel="preload" as="style" href="baz" />
1467
+ <link rel="preload" as="style" href="qux" />
1468
+ </body>
1469
+ </html>,
1470
+ );
1471
1869
- ReactDOMClient.hydrateRoot(
1870
- document,
1871
- <html>
1872
- <head>
1873
- <meta charSet="utf-8" />
1874
- </head>
1875
- <body>
1876
- <meta name="robots" content="noindex" />
1877
- <meta httpEquiv="content-security-policy" content="foo" />
1878
- <div>hello world</div>
1879
- </body>
1880
- </html>,
1881
- );
1882
- expect(Scheduler).toFlushWithoutYielding();
1883
- expect(getMeaningfulChildren(document)).toEqual(
1884
- <html>
1885
- <head>
1886
- <meta charset="utf-8" />
1887
- <meta name="robots" content="noindex" />
1888
- <meta http-equiv="content-security-policy" content="foo" />
1889
- <meta itemprop="description" content="desc" />
1890
- <meta property="description" content="desc2" />
1891
- </head>
1892
- <body>
1893
- <div>hello world</div>
1894
- </body>
1895
- </html>,
1896
- );
1472
+ await act(() => {
1473
+ resolveText('unblock');
1474
});
1475
1899
- // @gate enableFloat
1900
- it('can rendering title tags anywhere in the tree', async () => {
1901
- await actIntoEmptyDocument(() => {
1902
- const {pipe} = renderToPipeableStream(
1903
- <>
1904
- <title>before</title>
1905
- <>
1906
- <html>
1907
- <head>
1908
- <title>in head</title>
1909
- </head>
1910
- <body>
1911
- <div>
1912
- <title>during</title>
1913
- hello world
1914
- </div>
1915
- </body>
1916
- </html>
1917
- </>
1918
- <title>after</title>
1919
- </>,
1920
- );
1921
- pipe(writable);
1922
- });
1476
+ expect(getMeaningfulChildren(document)).toEqual(
1477
+ <html>
1478
+ <head>
1479
+ <link rel="stylesheet" href="initial" data-precedence="one" />
1480
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1481
+ <link rel="stylesheet" href="qux" data-precedence="one" />
1482
+ <link rel="stylesheet" href="preset" data-precedence="preset" />
1483
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1484
+ <link rel="stylesheet" href="baz" data-precedence="two" />
1485
+ <link rel="preload" href="foo" as="style" />
1486
+ </head>
1487
+ <body>
1488
+ <div>
1489
+ <div>foo</div>
1490
+ <div>bar</div>
1491
+ </div>
1492
+ <div>
1493
+ <div>bar</div>
1494
+ <div>baz</div>
1495
+ <div>qux</div>
1496
+ </div>
1497
+ <div>
1498
+ <div>bar</div>
1499
+ <div>baz</div>
1500
+ <div>qux</div>
1501
+ </div>
1502
+ <link rel="preload" as="style" href="bar" />
1503
+ <link rel="preload" as="style" href="baz" />
1504
+ <link rel="preload" as="style" href="qux" />
1505
+ </body>
1506
+ </html>,
1507
+ );
1508
+ });
1509
1924
- expect(getMeaningfulChildren(document)).toEqual(
1510
+ // @gate enableFloat
1511
+ it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => {
1512
+ await actIntoEmptyDocument(() => {
1513
+ const {pipe} = renderToPipeableStream(
1514
<html>
1926
- <head>
1927
- <title>before</title>
1928
- <title>in head</title>
1929
- <title>during</title>
1930
- <title>after</title>
1931
- </head>
1515
+ <head />
1516
<body>
1933
- <div>hello world</div>
1934
- </body>
1935
- </html>,
1936
- );
1937
-
1938
- ReactDOMClient.hydrateRoot(
1939
- document,
1940
- <>
1941
- <title>before</title>
1942
- <>
1943
- <html>
1944
- <head>
1945
- <title>in head</title>
1946
- </head>
1947
- <body>
1517
+ <div>
1518
+ outer
1519
+ <link rel="stylesheet" href="1one" precedence="one" />
1520
+ <link rel="stylesheet" href="1two" precedence="two" />
1521
+ <link rel="stylesheet" href="1three" precedence="three" />
1522
+ <link rel="stylesheet" href="1four" precedence="four" />
1523
+ <Suspense fallback={null}>
1524
<div>
1949
- <title>during</title>
1950
- hello world
1525
+ middle
1526
+ <link rel="stylesheet" href="2one" precedence="one" />
1527
+ <link rel="stylesheet" href="2two" precedence="two" />
1528
+ <link rel="stylesheet" href="2three" precedence="three" />
1529
+ <link rel="stylesheet" href="2four" precedence="four" />
1530
+ <Suspense fallback={null}>
1531
+ <div>
1532
+ inner
1533
+ <link rel="stylesheet" href="3five" precedence="five" />
1534
+ <link rel="stylesheet" href="3one" precedence="one" />
1535
+ <link rel="stylesheet" href="3two" precedence="two" />
1536
+ <link rel="stylesheet" href="3three" precedence="three" />
1537
+ <link rel="stylesheet" href="3four" precedence="four" />
1538
+ </div>
1539
+ </Suspense>
1540
</div>
1952
- </body>
1953
- </html>
1954
- </>
1955
- <title>after</title>
1956
- </>,
1957
- );
1958
- expect(Scheduler).toFlushWithoutYielding();
1959
- expect(getMeaningfulChildren(document)).toEqual(
1960
- <html>
1961
- <head>
1962
- <title>before</title>
1963
- <title>in head</title>
1964
- <title>during</title>
1965
- <title>after</title>
1966
- </head>
1967
- <body>
1968
- <div>hello world</div>
1541
+ </Suspense>
1542
+ <Suspense fallback={null}>
1543
+ <div>middle</div>
1544
+ <link rel="stylesheet" href="4one" precedence="one" />
1545
+ <link rel="stylesheet" href="4two" precedence="two" />
1546
+ <link rel="stylesheet" href="4three" precedence="three" />
1547
+ <link rel="stylesheet" href="4four" precedence="four" />
1548
+ </Suspense>
1549
+ </div>
1550
</body>
1551
</html>,
1552
);
1553
+ pipe(writable);
1554
});
1555
1974
- // @gate enableFloat
1975
- it('prepends new titles on the client so newer ones override older ones, including orphaned server rendered titles', async () => {
1976
- await actIntoEmptyDocument(() => {
1977
- const {pipe} = renderToPipeableStream(
1978
- <html>
1979
- <head>
1980
- <title>server</title>
1981
- </head>
1982
- <body>
1983
- <div>hello world</div>
1984
- </body>
1985
- </html>,
1986
- );
1987
- pipe(writable);
1988
- });
1556
+ expect(getMeaningfulChildren(document)).toEqual(
1557
+ <html>
1558
+ <head>
1559
+ <link rel="stylesheet" href="1one" data-precedence="one" />
1560
+ <link rel="stylesheet" href="2one" data-precedence="one" />
1561
+ <link rel="stylesheet" href="3one" data-precedence="one" />
1562
+ <link rel="stylesheet" href="4one" data-precedence="one" />
1563
1990
- expect(getMeaningfulChildren(document)).toEqual(
1991
- <html>
1992
- <head>
1993
- <title>server</title>
1994
- </head>
1995
- <body>
1996
- <div>hello world</div>
1997
- </body>
1998
- </html>,
1999
- );
1564
+ <link rel="stylesheet" href="1two" data-precedence="two" />
1565
+ <link rel="stylesheet" href="2two" data-precedence="two" />
1566
+ <link rel="stylesheet" href="3two" data-precedence="two" />
1567
+ <link rel="stylesheet" href="4two" data-precedence="two" />
1568
2001
- ReactDOMClient.hydrateRoot(
2002
- document,
2003
- <html>
2004
- <title>html</title>
2005
- <head>
2006
- <title>head</title>
2007
- </head>
2008
- <body>
2009
- <title>body</title>
2010
- <div>hello world</div>
2011
- </body>
2012
- </html>,
2013
- );
2014
- expect(Scheduler).toFlushWithoutYielding();
2015
- expect(getMeaningfulChildren(document)).toEqual(
1569
+ <link rel="stylesheet" href="1three" data-precedence="three" />
1570
+ <link rel="stylesheet" href="2three" data-precedence="three" />
1571
+ <link rel="stylesheet" href="3three" data-precedence="three" />
1572
+ <link rel="stylesheet" href="4three" data-precedence="three" />
1573
+
1574
+ <link rel="stylesheet" href="1four" data-precedence="four" />
1575
+ <link rel="stylesheet" href="2four" data-precedence="four" />
1576
+ <link rel="stylesheet" href="3four" data-precedence="four" />
1577
+ <link rel="stylesheet" href="4four" data-precedence="four" />
1578
+
1579
+ <link rel="stylesheet" href="3five" data-precedence="five" />
1580
+ </head>
1581
+ <body>
1582
+ <div>
1583
+ outer
1584
+ <div>
1585
+ middle<div>inner</div>
1586
+ </div>
1587
+ <div>middle</div>
1588
+ </div>
1589
+ </body>
1590
+ </html>,
1591
+ );
1592
+ });
1593
+
1594
+ // @gate enableFloat
1595
+ it('stylesheet resources are inserted according to precedence order on the client', async () => {
1596
+ await actIntoEmptyDocument(() => {
1597
+ const {pipe} = renderToPipeableStream(
1598
<html>
2017
- <head>
2018
- <title>body</title>
2019
- <title>head</title>
2020
- <title>html</title>
2021
- <title>server</title>
2022
- </head>
1599
+ <head />
1600
<body>
2024
- <div>hello world</div>
1601
+ <div>
1602
+ <link rel="stylesheet" href="foo" precedence="one" />
1603
+ <link rel="stylesheet" href="bar" precedence="two" />
1604
+ Hello
1605
+ </div>
1606
</body>
1607
</html>,
1608
);
1609
+ pipe(writable);
1610
});
1611
2030
- // @gate enableFloat
2031
- it('keys titles on text children and only removes them when no more instances refer to that title', async () => {
2032
- const root = ReactDOMClient.createRoot(container);
2033
- root.render(
2034
- <div>
2035
- <title>{[2]}</title>hello world<title>2</title>
2036
- </div>,
2037
- );
2038
- expect(Scheduler).toFlushWithoutYielding();
2039
- expect(getMeaningfulChildren(document)).toEqual(
2040
- <html>
2041
- <head>
2042
- <title>2</title>
2043
- </head>
2044
- <body>
2045
- <div id="container">
2046
- <div>hello world</div>
2047
- </div>
2048
- </body>
2049
- </html>,
2050
- );
1612
+ expect(getMeaningfulChildren(document)).toEqual(
1613
+ <html>
1614
+ <head>
1615
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1616
+ <link rel="stylesheet" href="bar" data-precedence="two" />
1617
+ </head>
1618
+ <body>
1619
+ <div>Hello</div>
1620
+ </body>
1621
+ </html>,
1622
+ );
1623
+
1624
+ const root = ReactDOMClient.hydrateRoot(
1625
+ document,
1626
+ <html>
1627
+ <head />
1628
+ <body>
1629
+ <div>
1630
+ <link rel="stylesheet" href="foo" precedence="one" />
1631
+ <link rel="stylesheet" href="bar" precedence="two" />
1632
+ Hello
1633
+ </div>
1634
+ </body>
1635
+ </html>,
1636
+ );
1637
+ expect(Scheduler).toFlushWithoutYielding();
1638
+ expect(getMeaningfulChildren(document)).toEqual(
1639
+ <html>
1640
+ <head>
1641
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1642
+ <link rel="stylesheet" href="bar" data-precedence="two" />
1643
+ </head>
1644
+ <body>
1645
+ <div>Hello</div>
1646
+ </body>
1647
+ </html>,
1648
+ );
1649
+
1650
+ root.render(
1651
+ <html>
1652
+ <head />
1653
+ <body>
1654
+ <div>Hello</div>
1655
+ <link rel="stylesheet" href="baz" precedence="one" />
1656
+ </body>
1657
+ </html>,
1658
+ );
1659
+ expect(Scheduler).toFlushWithoutYielding();
1660
+ expect(getMeaningfulChildren(document)).toEqual(
1661
+ <html>
1662
+ <head>
1663
+ <link rel="stylesheet" href="foo" data-precedence="one" />
1664
+ <link rel="stylesheet" href="baz" data-precedence="one" />
1665
+ <link rel="stylesheet" href="bar" data-precedence="two" />
1666
+ <link rel="preload" as="style" href="baz" />
1667
+ </head>
1668
+ <body>
1669
+ <div>Hello</div>
1670
+ </body>
1671
+ </html>,
1672
+ );
1673
+ });
1674
+
1675
+ // @gate enableFloat
1676
+ it('inserts preloads in render phase eagerly', async () => {
1677
+ function Throw() {
1678
+ throw new Error('Uh oh!');
1679
+ }
1680
+ class ErrorBoundary extends React.Component {
1681
+ state = {hasError: false, error: null};
1682
+ static getDerivedStateFromError(error) {
1683
+ return {
1684
+ hasError: true,
1685
+ error,
1686
+ };
1687
+ }
1688
+ render() {
1689
+ if (this.state.hasError) {
1690
+ return this.state.error.message;
1691
+ }
1692
+ return this.props.children;
1693
+ }
1694
+ }
1695
2052
- root.render(
2053
- <div>
2054
- {null}hello world<title>2</title>
2055
- </div>,
2056
- );
2057
- expect(Scheduler).toFlushWithoutYielding();
2058
- expect(getMeaningfulChildren(document)).toEqual(
2059
- <html>
2060
- <head>
2061
- <title>2</title>
2062
- </head>
2063
- <body>
2064
- <div id="container">
2065
- <div>hello world</div>
2066
- </div>
2067
- </body>
2068
- </html>,
2069
- );
2070
- root.render(
2071
- <div>
2072
- {null}hello world{null}
2073
- </div>,
2074
- );
2075
- expect(Scheduler).toFlushWithoutYielding();
2076
- expect(getMeaningfulChildren(document)).toEqual(
1696
+ const root = ReactDOMClient.createRoot(container);
1697
+ root.render(
1698
+ <ErrorBoundary>
1699
+ <link rel="stylesheet" href="foo" precedence="default" />
1700
+ <div>foo</div>
1701
+ <Throw />
1702
+ </ErrorBoundary>,
1703
+ );
1704
+ expect(Scheduler).toFlushWithoutYielding();
1705
+ expect(getMeaningfulChildren(document)).toEqual(
1706
+ <html>
1707
+ <head>
1708
+ <link rel="preload" href="foo" as="style" />
1709
+ </head>
1710
+ <body>
1711
+ <div id="container">Uh oh!</div>
1712
+ </body>
1713
+ </html>,
1714
+ );
1715
+ });
1716
+
1717
+ // @gate enableFloat
1718
+ it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => {
1719
+ await actIntoEmptyDocument(() => {
1720
+ const {pipe} = renderToPipeableStream(
1721
<html>
1722
<head />
1723
<body>
2080
- <div id="container">
2081
- <div>hello world</div>
1724
+ <div>
1725
+ <Suspense fallback="loading foo...">
1726
+ <BlockedOn value="foo">
1727
+ <div>foo</div>
1728
+ <link rel="stylesheet" href="foo" precedence="default" />
1729
+ <Suspense fallback="loading bar...">
1730
+ <BlockedOn value="bar">
1731
+ <div>bar</div>
1732
+ <link rel="stylesheet" href="bar" precedence="default" />
1733
+ <Suspense fallback="loading baz...">
1734
+ <BlockedOn value="baz">
1735
+ <div>baz</div>
1736
+ <link
1737
+ rel="stylesheet"
1738
+ href="baz"
1739
+ precedence="default"
1740
+ />
1741
+ </BlockedOn>
1742
+ </Suspense>
1743
+ </BlockedOn>
1744
+ </Suspense>
1745
+ </BlockedOn>
1746
+ </Suspense>
1747
</div>
1748
</body>
1749
</html>,
1750
);
1751
+ pipe(writable);
1752
});
1753
2088
- // @gate enableFloat && enableHostSingletons && (enableClientRenderFallbackOnTextMismatch || !__DEV__)
2089
- it('can render a title before a singleton even if that singleton clears its contents', async () => {
2090
- await actIntoEmptyDocument(() => {
2091
- const {pipe} = renderToPipeableStream(
2092
- <>
2093
- <title>foo</title>
2094
- <html>
2095
- <head />
2096
- <body>
2097
- <div>server</div>
2098
- </body>
2099
- </html>
2100
- </>,
2101
- );
2102
- pipe(writable);
2103
- });
1754
+ expect(getMeaningfulChildren(document)).toEqual(
1755
+ <html>
1756
+ <head />
1757
+ <body>
1758
+ <div>loading foo...</div>
1759
+ </body>
1760
+ </html>,
1761
+ );
1762
2105
- const errors = [];
2106
- ReactDOMClient.hydrateRoot(
2107
- document,
2108
- <>
2109
- <title>foo</title>
2110
- <html>
2111
- <head />
2112
- <body>
2113
- <div>client</div>
2114
- </body>
2115
- </html>
2116
- </>,
2117
- {
2118
- onRecoverableError(err) {
2119
- errors.push(err.message);
2120
- },
2121
- },
2122
- );
2123
- try {
2124
- expect(() => {
2125
- expect(Scheduler).toFlushWithoutYielding();
2126
- }).toErrorDev(
2127
- [
2128
- 'Warning: Text content did not match. Server: "server" Client: "client"',
2129
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2130
- ],
2131
- {withoutStack: 1},
2132
- );
2133
- } catch (e) {
2134
- // When gates are false this test fails on a DOMException if you don't clear the scheduler after catching.
2135
- // When gates are true this branch should not be hit
2136
- expect(Scheduler).toFlushWithoutYielding();
2137
- throw e;
2138
- }
2139
- expect(getMeaningfulChildren(document)).toEqual(
2140
- <html>
2141
- <head>
2142
- <title>foo</title>
2143
- </head>
2144
- <body>
2145
- <div>client</div>
2146
- </body>
2147
- </html>,
2148
- );
1763
+ await act(() => {
1764
+ resolveText('bar');
1765
});
2150
- });
1766
+ expect(getMeaningfulChildren(document)).toEqual(
1767
+ <html>
1768
+ <head />
1769
+ <body>
1770
+ <div>loading foo...</div>
1771
+ </body>
1772
+ </html>,
1773
+ );
1774
2152
- describe('style resources', () => {
2153
- // @gate enableFloat
2154
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
2155
- await actIntoEmptyDocument(() => {
2156
- const {pipe} = renderToPipeableStream(
2157
- <html>
2158
- <head />
2159
- <body>
2160
- <link rel="stylesheet" href="aresource" precedence="foo" />
2161
- <div>hello world</div>
2162
- </body>
2163
- </html>,
2164
- );
2165
- pipe(writable);
2166
- });
1775
+ await act(() => {
1776
+ resolveText('baz');
1777
+ });
1778
+ expect(getMeaningfulChildren(document)).toEqual(
1779
+ <html>
1780
+ <head />
1781
+ <body>
1782
+ <div>loading foo...</div>
1783
+ </body>
1784
+ </html>,
1785
+ );
1786
2168
- expect(getMeaningfulChildren(document)).toEqual(
2169
- <html>
2170
- <head>
2171
- <link rel="stylesheet" href="aresource" data-precedence="foo" />
2172
- </head>
2173
- <body>
2174
- <div>hello world</div>
2175
- </body>
2176
- </html>,
1787
+ await act(() => {
1788
+ resolveText('foo');
1789
+ });
1790
+ expect(getMeaningfulChildren(document)).toEqual(
1791
+ <html>
1792
+ <head>
1793
+ <link rel="stylesheet" href="foo" data-precedence="default" />
1794
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1795
+ <link rel="stylesheet" href="baz" data-precedence="default" />
1796
+ </head>
1797
+ <body>
1798
+ <div>loading foo...</div>
1799
+ <link rel="preload" href="foo" as="style" />
1800
+ <link rel="preload" href="bar" as="style" />
1801
+ <link rel="preload" href="baz" as="style" />
1802
+ </body>
1803
+ </html>,
1804
+ );
1805
+
1806
+ await act(() => {
1807
+ const event = document.createEvent('Events');
1808
+ event.initEvent('load', true, true);
1809
+ Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1810
+ el => {
1811
+ el.dispatchEvent(event);
1812
+ },
1813
);
1814
});
1815
+ expect(getMeaningfulChildren(document)).toEqual(
1816
+ <html>
1817
+ <head>
1818
+ <link rel="stylesheet" href="foo" data-precedence="default" />
1819
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1820
+ <link rel="stylesheet" href="baz" data-precedence="default" />
1821
+ </head>
1822
+ <body>
1823
+ <div>
1824
+ <div>foo</div>
1825
+ <div>bar</div>
1826
+ <div>baz</div>
1827
+ </div>
1828
+ <link rel="preload" href="foo" as="style" />
1829
+ <link rel="preload" href="bar" as="style" />
1830
+ <link rel="preload" href="baz" as="style" />
1831
+ </body>
1832
+ </html>,
1833
+ );
1834
+ });
1835
2180
- // @gate enableFloat
2181
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when client rendering', async () => {
2182
- const root = ReactDOMClient.createRoot(document);
2183
- root.render(
1836
+ // @gate enableFloat
1837
+ it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
1838
+ await actIntoEmptyDocument(() => {
1839
+ const {pipe} = renderToPipeableStream(
1840
<html>
1841
<head />
1842
<body>
2187
- <link rel="stylesheet" href="aresource" precedence="foo" />
2188
- <div>hello world</div>
1843
+ <div>
1844
+ <Suspense fallback="loading...">
1845
+ <div>
1846
+ <BlockedOn value="foo">
1847
+ <div>foo</div>
1848
+ <link rel="stylesheet" href="foo" precedence="default" />
1849
+ <Suspense fallback="loading bar...">
1850
+ <BlockedOn value="bar">
1851
+ <div>bar</div>
1852
+ <link
1853
+ rel="stylesheet"
1854
+ href="bar"
1855
+ precedence="default"
1856
+ />
1857
+ <Suspense fallback="loading baz...">
1858
+ <div>
1859
+ <BlockedOn value="baz">
1860
+ <div>baz</div>
1861
+ <link
1862
+ rel="stylesheet"
1863
+ href="baz"
1864
+ precedence="default"
1865
+ />
1866
+ </BlockedOn>
1867
+ </div>
1868
+ </Suspense>
1869
+ </BlockedOn>
1870
+ </Suspense>
1871
+ </BlockedOn>
1872
+ <BlockedOn value="qux">
1873
+ <div>qux</div>
1874
+ <link rel="stylesheet" href="qux" precedence="default" />
1875
+ </BlockedOn>
1876
+ </div>
1877
+ </Suspense>
1878
+ </div>
1879
</body>
1880
</html>,
1881
);
2192
- expect(Scheduler).toFlushWithoutYielding();
1882
+ pipe(writable);
1883
+ });
1884
2194
- expect(getMeaningfulChildren(document)).toEqual(
2195
- <html>
2196
- <head>
2197
- <link rel="stylesheet" href="aresource" data-precedence="foo" />
2198
- </head>
2199
- <body>
2200
- <div>hello world</div>
2201
- </body>
2202
- </html>,
2203
- );
1885
+ expect(getMeaningfulChildren(document)).toEqual(
1886
+ <html>
1887
+ <head />
1888
+ <body>
1889
+ <div>loading...</div>
1890
+ </body>
1891
+ </html>,
1892
+ );
1893
+
1894
+ // This will enqueue a stylesheet resource in a deep blocked boundary (loading baz...).
1895
+ await act(() => {
1896
+ resolveText('baz');
1897
});
1898
+ expect(getMeaningfulChildren(document)).toEqual(
1899
+ <html>
1900
+ <head />
1901
+ <body>
1902
+ <div>loading...</div>
1903
+ </body>
1904
+ </html>,
1905
+ );
1906
2206
- // @gate enableFloat
2207
- it('treats link rel stylesheet elements as a style resource when it includes a precedence when hydrating', async () => {
2208
- await actIntoEmptyDocument(() => {
2209
- const {pipe} = renderToPipeableStream(
2210
- <html>
2211
- <head />
2212
- <body>
2213
- <link rel="stylesheet" href="aresource" precedence="foo" />
2214
- <div>hello world</div>
2215
- </body>
2216
- </html>,
2217
- );
2218
- pipe(writable);
2219
- });
2220
- ReactDOMClient.hydrateRoot(
2221
- document,
2222
- <html>
2223
- <head />
2224
- <body>
2225
- <link rel="stylesheet" href="aresource" precedence="foo" />
2226
- <div>hello world</div>
2227
- </body>
2228
- </html>,
1907
+ // This will enqueue a stylesheet resource in the intermediate blocked boundary (loading bar...).
1908
+ await act(() => {
1909
+ resolveText('bar');
1910
+ });
1911
+ expect(getMeaningfulChildren(document)).toEqual(
1912
+ <html>
1913
+ <head />
1914
+ <body>
1915
+ <div>loading...</div>
1916
+ </body>
1917
+ </html>,
1918
+ );
1919
+
1920
+ // This will complete a segment in the top level boundary that is still blocked on another segment.
1921
+ // It will flush the completed segment however the inner boundaries should not emit their style dependencies
1922
+ // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked
1923
+ // boundary (top level).
1924
+ await act(() => {
1925
+ resolveText('foo');
1926
+ });
1927
+ expect(getMeaningfulChildren(document)).toEqual(
1928
+ <html>
1929
+ <head />
1930
+ <body>
1931
+ <div>loading...</div>
1932
+ <link rel="preload" href="foo" as="style" />
1933
+ <link rel="preload" href="bar" as="style" />
1934
+ <link rel="preload" href="baz" as="style" />
1935
+ </body>
1936
+ </html>,
1937
+ );
1938
+
1939
+ // This resolves the last blocked segment on the top level boundary so we see all dependencies of the
1940
+ // nested boundaries emitted at this level
1941
+ await act(() => {
1942
+ resolveText('qux');
1943
+ });
1944
+ expect(getMeaningfulChildren(document)).toEqual(
1945
+ <html>
1946
+ <head>
1947
+ <link rel="stylesheet" href="foo" data-precedence="default" />
1948
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1949
+ <link rel="stylesheet" href="baz" data-precedence="default" />
1950
+ <link rel="stylesheet" href="qux" data-precedence="default" />
1951
+ </head>
1952
+ <body>
1953
+ <div>loading...</div>
1954
+ <link rel="preload" href="foo" as="style" />
1955
+ <link rel="preload" href="bar" as="style" />
1956
+ <link rel="preload" href="baz" as="style" />
1957
+ <link rel="preload" href="qux" as="style" />
1958
+ </body>
1959
+ </html>,
1960
+ );
1961
+
1962
+ // We load all stylesheets and confirm the content is revealed
1963
+ await act(() => {
1964
+ const event = document.createEvent('Events');
1965
+ event.initEvent('load', true, true);
1966
+ Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1967
+ el => {
1968
+ el.dispatchEvent(event);
1969
+ },
1970
);
2230
- expect(Scheduler).toFlushWithoutYielding();
1971
+ });
1972
+ expect(getMeaningfulChildren(document)).toEqual(
1973
+ <html>
1974
+ <head>
1975
+ <link rel="stylesheet" href="foo" data-precedence="default" />
1976
+ <link rel="stylesheet" href="bar" data-precedence="default" />
1977
+ <link rel="stylesheet" href="baz" data-precedence="default" />
1978
+ <link rel="stylesheet" href="qux" data-precedence="default" />
1979
+ </head>
1980
+ <body>
1981
+ <div>
1982
+ <div>
1983
+ <div>foo</div>
1984
+ <div>bar</div>
1985
+ <div>
1986
+ <div>baz</div>
1987
+ </div>
1988
+ <div>qux</div>
1989
+ </div>
1990
+ </div>
1991
+ <link rel="preload" href="foo" as="style" />
1992
+ <link rel="preload" href="bar" as="style" />
1993
+ <link rel="preload" href="baz" as="style" />
1994
+ <link rel="preload" href="qux" as="style" />
1995
+ </body>
1996
+ </html>,
1997
+ );
1998
+ });
1999
2232
- expect(getMeaningfulChildren(document)).toEqual(
2000
+ // @gate enableFloat
2001
+ it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => {
2002
+ function App() {
2003
+ return (
2004
<html>
2234
- <head>
2235
- <link rel="stylesheet" href="aresource" data-precedence="foo" />
2236
- </head>
2005
+ <head />
2006
<body>
2238
- <div>hello world</div>
2007
+ <div>
2008
+ <link
2009
+ // This preload is explicit so it can flush with a lot of potential attrs
2010
+ // We will duplicate this as a style that flushes after the shell
2011
+ rel="stylesheet"
2012
+ href="foo"
2013
+ // precedence is not a special attribute for preloads so this will just flush as is
2014
+ precedence="default"
2015
+ // Some standard link props
2016
+ crossOrigin="anonymous"
2017
+ media="all"
2018
+ integrity="somehash"
2019
+ referrerPolicy="origin"
2020
+ // data and non starndard attributes that should flush
2021
+ data-foo={'"quoted"'}
2022
+ nonStandardAttr="attr"
2023
+ properlyformattednonstandardattr="attr"
2024
+ // attributes that should be filtered out for violating certain rules
2025
+ onSomething="this should be removed b/c event handler"
2026
+ shouldnotincludefunctions={() => {}}
2027
+ norsymbols={Symbol('foo')}
2028
+ />
2029
+ <Suspense fallback={'loading...'}>
2030
+ <BlockedOn value="unblock">
2031
+ <link
2032
+ // This preload is explicit so it can flush with a lot of potential attrs
2033
+ // We will duplicate this as a style that flushes after the shell
2034
+ rel="stylesheet"
2035
+ href="bar"
2036
+ // opt-in property to get this treated as a resource
2037
+ precedence="default"
2038
+ // Some standard link props
2039
+ crossOrigin="anonymous"
2040
+ media="all"
2041
+ integrity="somehash"
2042
+ referrerPolicy="origin"
2043
+ // data and non starndard attributes that should flush
2044
+ data-foo={'"quoted"'}
2045
+ nonStandardAttr="attr"
2046
+ properlyformattednonstandardattr="attr"
2047
+ // attributes that should be filtered out for violating certain rules
2048
+ onSomething="this should be removed b/c event handler"
2049
+ shouldnotincludefunctions={() => {}}
2050
+ norsymbols={Symbol('foo')}
2051
+ />
2052
+ </BlockedOn>
2053
+ </Suspense>
2054
+ </div>
2055
</body>
2240
- </html>,
2056
+ </html>
2057
);
2242
- });
2243
-
2244
- // @gate enableFloat
2245
- it('preloads stylesheets without a precedence prop when server rendering', async () => {
2058
+ }
2059
+ await expect(async () => {
2060
await actIntoEmptyDocument(() => {
2247
- const {pipe} = renderToPipeableStream(
2248
- <html>
2249
- <head />
2250
- <body>
2251
- <link rel="stylesheet" href="notaresource" />
2252
- <div>hello world</div>
2253
- </body>
2254
- </html>,
2255
- );
2061
+ const {pipe} = renderToPipeableStream(<App />);
2062
pipe(writable);
2063
});
2258
-
2064
expect(getMeaningfulChildren(document)).toEqual(
2065
<html>
2066
<head>
2262
- <link rel="preload" as="style" href="notaresource" />
2067
+ <link
2068
+ rel="stylesheet"
2069
+ href="foo"
2070
+ data-precedence="default"
2071
+ crossorigin="anonymous"
2072
+ media="all"
2073
+ integrity="somehash"
2074
+ referrerpolicy="origin"
2075
+ data-foo={'"quoted"'}
2076
+ nonstandardattr="attr"
2077
+ properlyformattednonstandardattr="attr"
2078
+ />
2079
</head>
2080
<body>
2265
- <link rel="stylesheet" href="notaresource" />
2266
- <div>hello world</div>
2081
+ <div>loading...</div>
2082
</body>
2083
</html>,
2084
);
2085
+ }).toErrorDev([
2086
+ 'React does not recognize the `nonStandardAttr` prop on a DOM element.' +
2087
+ ' If you intentionally want it to appear in the DOM as a custom attribute,' +
2088
+ ' spell it as lowercase `nonstandardattr` instead. If you accidentally passed it from a' +
2089
+ ' parent component, remove it from the DOM element.',
2090
+ 'Invalid values for props `shouldnotincludefunctions`, `norsymbols` on <link> tag. Either remove them from' +
2091
+ ' the element, or pass a string or number value to keep them in the DOM. For' +
2092
+ ' details, see https://reactjs.org/link/attribute-behavior',
2093
+ ]);
2094
+
2095
+ // Now we flush the stylesheet with the boundary
2096
+ await act(() => {
2097
+ resolveText('unblock');
2098
});
2099
2272
- // @gate enableFloat
2273
- it('hoists style resources to the correct precedence', async () => {
2274
- await actIntoEmptyDocument(() => {
2275
- const {pipe} = renderToPipeableStream(
2276
- <html>
2277
- <head />
2278
- <body>
2279
- <link rel="stylesheet" href="foo1" precedence="foo" />
2280
- <link rel="stylesheet" href="default1" precedence="default" />
2281
- <link rel="stylesheet" href="foo2" precedence="foo" />
2282
- <div>hello world</div>
2283
- </body>
2284
- </html>,
2285
- );
2286
- pipe(writable);
2287
- });
2288
- expect(getMeaningfulChildren(document)).toEqual(
2289
- <html>
2290
- <head>
2291
- <link rel="stylesheet" href="foo1" data-precedence="foo" />
2292
- <link rel="stylesheet" href="foo2" data-precedence="foo" />
2293
- <link rel="stylesheet" href="default1" data-precedence="default" />
2294
- </head>
2295
- <body>
2296
- <div>hello world</div>
2297
- </body>
2298
- </html>,
2299
- );
2100
+ expect(getMeaningfulChildren(document)).toEqual(
2101
+ <html>
2102
+ <head>
2103
+ <link
2104
+ rel="stylesheet"
2105
+ href="foo"
2106
+ data-precedence="default"
2107
+ crossorigin="anonymous"
2108
+ media="all"
2109
+ integrity="somehash"
2110
+ referrerpolicy="origin"
2111
+ data-foo={'"quoted"'}
2112
+ nonstandardattr="attr"
2113
+ properlyformattednonstandardattr="attr"
2114
+ />
2115
+ <link
2116
+ rel="stylesheet"
2117
+ href="bar"
2118
+ data-precedence="default"
2119
+ crossorigin="anonymous"
2120
+ media="all"
2121
+ integrity="somehash"
2122
+ referrerpolicy="origin"
2123
+ data-foo={'"quoted"'}
2124
+ nonstandardattr="attr"
2125
+ properlyformattednonstandardattr="attr"
2126
+ />
2127
+ </head>
2128
+ <body>
2129
+ <div>loading...</div>
2130
+ <link
2131
+ rel="preload"
2132
+ as="style"
2133
+ href="bar"
2134
+ crossorigin="anonymous"
2135
+ media="all"
2136
+ integrity="somehash"
2137
+ referrerpolicy="origin"
2138
+ />
2139
+ </body>
2140
+ </html>,
2141
+ );
2142
+ });
2143
2301
- ReactDOMClient.hydrateRoot(
2302
- document,
2144
+ // @gate enableFloat
2145
+ it('boundary stylesheet resource dependencies hoist to a parent boundary when flushed inline', async () => {
2146
+ await actIntoEmptyDocument(() => {
2147
+ const {pipe} = renderToPipeableStream(
2148
<html>
2149
<head />
2150
<body>
2306
- <link rel="stylesheet" href="bar1" precedence="bar" />
2307
- <link rel="stylesheet" href="foo3" precedence="foo" />
2308
- <link rel="stylesheet" href="default2" precedence="default" />
2309
- <div>hello world</div>
2310
- </body>
2311
- </html>,
2312
- );
2313
- expect(Scheduler).toFlushWithoutYielding();
2314
- expect(getMeaningfulChildren(document)).toEqual(
2315
- <html>
2316
- <head>
2317
- <link rel="stylesheet" href="foo1" data-precedence="foo" />
2318
- <link rel="stylesheet" href="foo2" data-precedence="foo" />
2319
- <link rel="stylesheet" href="foo3" data-precedence="foo" />
2320
- <link rel="stylesheet" href="default1" data-precedence="default" />
2321
- <link rel="stylesheet" href="default2" data-precedence="default" />
2322
- <link rel="stylesheet" href="bar1" data-precedence="bar" />
2323
- <link rel="preload" as="style" href="bar1" />
2324
- <link rel="preload" as="style" href="foo3" />
2325
- <link rel="preload" as="style" href="default2" />
2326
- </head>
2327
- <body>
2328
- <div>hello world</div>
2151
+ <div>
2152
+ <Suspense fallback="loading A...">
2153
+ <BlockedOn value="unblock">
2154
+ <AsyncText text="A" />
2155
+ <link rel="stylesheet" href="A" precedence="A" />
2156
+ <Suspense fallback="loading AA...">
2157
+ <AsyncText text="AA" />
2158
+ <link rel="stylesheet" href="AA" precedence="AA" />
2159
+ <Suspense fallback="loading AAA...">
2160
+ <AsyncText text="AAA" />
2161
+ <link rel="stylesheet" href="AAA" precedence="AAA" />
2162
+ <Suspense fallback="loading AAAA...">
2163
+ <AsyncText text="AAAA" />
2164
+ <link rel="stylesheet" href="AAAA" precedence="AAAA" />
2165
+ </Suspense>
2166
+ </Suspense>
2167
+ </Suspense>
2168
+ </BlockedOn>
2169
+ </Suspense>
2170
+ </div>
2171
</body>
2172
</html>,
2173
);
2174
+ pipe(writable);
2175
});
2176
+ expect(getMeaningfulChildren(document)).toEqual(
2177
+ <html>
2178
+ <head />
2179
+ <body>
2180
+ <div>loading A...</div>
2181
+ </body>
2182
+ </html>,
2183
+ );
2184
2334
- // @gate enableFloat
2335
- it('retains styles even after the last referring Resource unmounts', async () => {
2336
- // This test is true until a future update where there is some form of garbage collection.
2337
- const root = ReactDOMClient.createRoot(document);
2185
+ await act(() => {
2186
+ resolveText('unblock');
2187
+ resolveText('AAAA');
2188
+ resolveText('AA');
2189
+ });
2190
+ expect(getMeaningfulChildren(document)).toEqual(
2191
+ <html>
2192
+ <head />
2193
+ <body>
2194
+ <div>loading A...</div>
2195
+ <link rel="preload" as="style" href="A" />
2196
+ <link rel="preload" as="style" href="AA" />
2197
+ <link rel="preload" as="style" href="AAA" />
2198
+ <link rel="preload" as="style" href="AAAA" />
2199
+ </body>
2200
+ </html>,
2201
+ );
2202
2339
- root.render(
2340
- <html>
2341
- <head />
2342
- <body>
2343
- hello world
2344
- <link rel="stylesheet" href="foo" precedence="foo" />
2345
- </body>
2346
- </html>,
2347
- );
2348
- expect(Scheduler).toFlushWithoutYielding();
2203
+ await act(() => {
2204
+ resolveText('A');
2205
+ });
2206
+ await act(() => {
2207
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2208
+ const event = document.createEvent('Events');
2209
+ event.initEvent('load', true, true);
2210
+ l.dispatchEvent(event);
2211
+ });
2212
+ });
2213
+ expect(getMeaningfulChildren(document)).toEqual(
2214
+ <html>
2215
+ <head>
2216
+ <link rel="stylesheet" href="A" data-precedence="A" />
2217
+ <link rel="stylesheet" href="AA" data-precedence="AA" />
2218
+ </head>
2219
+ <body>
2220
+ <div>
2221
+ {'A'}
2222
+ {'AA'}
2223
+ {'loading AAA...'}
2224
+ </div>
2225
+ <link rel="preload" as="style" href="A" />
2226
+ <link rel="preload" as="style" href="AA" />
2227
+ <link rel="preload" as="style" href="AAA" />
2228
+ <link rel="preload" as="style" href="AAAA" />
2229
+ </body>
2230
+ </html>,
2231
+ );
2232
2350
- root.render(
2351
- <html>
2352
- <head />
2353
- <body>hello world</body>
2354
- </html>,
2355
- );
2356
- expect(Scheduler).toFlushWithoutYielding();
2357
- expect(getMeaningfulChildren(document)).toEqual(
2233
+ await act(() => {
2234
+ resolveText('AAA');
2235
+ });
2236
+ await act(() => {
2237
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2238
+ const event = document.createEvent('Events');
2239
+ event.initEvent('load', true, true);
2240
+ l.dispatchEvent(event);
2241
+ });
2242
+ });
2243
+ expect(getMeaningfulChildren(document)).toEqual(
2244
+ <html>
2245
+ <head>
2246
+ <link rel="stylesheet" href="A" data-precedence="A" />
2247
+ <link rel="stylesheet" href="AA" data-precedence="AA" />
2248
+ <link rel="stylesheet" href="AAA" data-precedence="AAA" />
2249
+ <link rel="stylesheet" href="AAAA" data-precedence="AAAA" />
2250
+ </head>
2251
+ <body>
2252
+ <div>
2253
+ {'A'}
2254
+ {'AA'}
2255
+ {'AAA'}
2256
+ {'AAAA'}
2257
+ </div>
2258
+ <link rel="preload" as="style" href="A" />
2259
+ <link rel="preload" as="style" href="AA" />
2260
+ <link rel="preload" as="style" href="AAA" />
2261
+ <link rel="preload" as="style" href="AAAA" />
2262
+ </body>
2263
+ </html>,
2264
+ );
2265
+ });
2266
+
2267
+ // @gate enableFloat
2268
+ it('always enforces crossOrigin "anonymous" for font preloads', async () => {
2269
+ function App() {
2270
+ ReactDOM.preload('foo', {as: 'font'});
2271
+ ReactDOM.preload('bar', {as: 'font', crossOrigin: 'foo'});
2272
+ ReactDOM.preload('baz', {as: 'font', crossOrigin: 'use-credentials'});
2273
+ ReactDOM.preload('qux', {as: 'font', crossOrigin: 'anonymous'});
2274
+ return (
2275
<html>
2359
- <head>
2360
- <link rel="stylesheet" href="foo" data-precedence="foo" />
2361
- </head>
2362
- <body>hello world</body>
2363
- </html>,
2276
+ <head />
2277
+ <body />
2278
+ </html>
2279
);
2280
+ }
2281
+ await actIntoEmptyDocument(() => {
2282
+ const {pipe} = renderToPipeableStream(<App />);
2283
+ pipe(writable);
2284
});
2285
+ expect(getMeaningfulChildren(document)).toEqual(
2286
+ <html>
2287
+ <head>
2288
+ <link rel="preload" as="font" href="foo" crossorigin="" />
2289
+ <link rel="preload" as="font" href="bar" crossorigin="" />
2290
+ <link rel="preload" as="font" href="baz" crossorigin="" />
2291
+ <link rel="preload" as="font" href="qux" crossorigin="" />
2292
+ </head>
2293
+ <body />
2294
+ </html>,
2295
+ );
2296
+ });
2297
2367
- // @gate enableFloat && enableHostSingletons && enableClientRenderFallbackOnTextMismatch
2368
- it('retains styles even when a new html, head, and/body mount', async () => {
2369
- await actIntoEmptyDocument(() => {
2370
- const {pipe} = renderToPipeableStream(
2298
+ describe('ReactDOM.preload(href, { as: ... })', () => {
2299
+ // @gate enableFloat
2300
+ it('creates a preload resource when called', async () => {
2301
+ function App() {
2302
+ ReactDOM.preload('foo', {as: 'style'});
2303
+ return (
2304
<html>
2372
- <head />
2305
<body>
2374
- <link rel="stylesheet" href="foo" precedence="foo" />
2375
- <link rel="stylesheet" href="bar" precedence="bar" />
2376
- server
2306
+ <Suspense fallback="loading...">
2307
+ <BlockedOn value="blocked">
2308
+ <Component />
2309
+ </BlockedOn>
2310
+ </Suspense>
2311
</body>
2378
- </html>,
2312
+ </html>
2313
);
2314
+ }
2315
+ function Component() {
2316
+ ReactDOM.preload('bar', {as: 'script'});
2317
+ return <div>hello</div>;
2318
+ }
2319
+
2320
+ await actIntoEmptyDocument(() => {
2321
+ const {pipe} = renderToPipeableStream(<App />);
2322
pipe(writable);
2323
});
2382
- const errors = [];
2383
- ReactDOMClient.hydrateRoot(
2384
- document,
2324
+ expect(getMeaningfulChildren(document)).toEqual(
2325
<html>
2326
<head>
2387
- <link rel="stylesheet" href="qux" precedence="qux" />
2388
- <link rel="stylesheet" href="foo" precedence="foo" />
2327
+ <link rel="preload" as="style" href="foo" />
2328
</head>
2390
- <body>client</body>
2329
+ <body>loading...</body>
2330
</html>,
2392
- {
2393
- onRecoverableError(error) {
2394
- errors.push(error.message);
2395
- },
2396
- },
2397
- );
2398
- expect(() => {
2399
- expect(Scheduler).toFlushWithoutYielding();
2400
- }).toErrorDev(
2401
- [
2402
- 'Warning: Text content did not match. Server: "server" Client: "client"',
2403
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2404
- ],
2405
- {withoutStack: 1},
2331
);
2332
+
2333
+ await act(() => {
2334
+ resolveText('blocked');
2335
+ });
2336
expect(getMeaningfulChildren(document)).toEqual(
2337
<html>
2338
<head>
2410
- <link rel="stylesheet" href="foo" data-precedence="foo" />
2411
- <link rel="stylesheet" href="bar" data-precedence="bar" />
2412
- <link rel="stylesheet" href="qux" data-precedence="qux" />
2339
+ <link rel="preload" as="style" href="foo" />
2340
</head>
2414
- <body>client</body>
2341
+ <body>
2342
+ <div>hello</div>
2343
+ <link rel="preload" as="script" href="bar" />
2344
+ </body>
2345
</html>,
2346
);
2417
- });
2347
2419
- // @gate enableFloat && !enableHostSingletons
2420
- it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
2421
- await actIntoEmptyDocument(() => {
2422
- const {pipe} = renderToPipeableStream(
2348
+ function ClientApp() {
2349
+ ReactDOM.preload('foo', {as: 'style'});
2350
+ React.useInsertionEffect(() => ReactDOM.preload('bar', {as: 'script'}));
2351
+ React.useLayoutEffect(() => ReactDOM.preload('baz', {as: 'font'}));
2352
+ React.useEffect(() => ReactDOM.preload('qux', {as: 'style'}));
2353
+ return (
2354
<html>
2424
- <head />
2355
<body>
2426
- <link rel="stylesheet" href="foo" precedence="foo" />
2427
- <link rel="stylesheet" href="bar" precedence="bar" />
2428
- server
2356
+ <Suspense fallback="loading...">
2357
+ <div>hello</div>
2358
+ </Suspense>
2359
</body>
2430
- </html>,
2360
+ </html>
2361
);
2432
- pipe(writable);
2433
- });
2434
- const errors = [];
2435
- ReactDOMClient.hydrateRoot(
2436
- document,
2437
- <html>
2438
- <head>
2439
- <link rel="stylesheet" href="qux" precedence="qux" />
2440
- <link rel="stylesheet" href="foo" precedence="foo" />
2441
- </head>
2442
- <body>client</body>
2443
- </html>,
2444
- {
2445
- onRecoverableError(error) {
2446
- errors.push(error.message);
2447
- },
2448
- },
2449
- );
2450
- expect(() => {
2451
- expect(Scheduler).toFlushWithoutYielding();
2452
- }).toErrorDev(
2453
- [
2454
- 'Warning: Text content did not match. Server: "server" Client: "client"',
2455
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2456
- ],
2457
- {withoutStack: 1},
2458
- );
2362
+ }
2363
+ ReactDOMClient.hydrateRoot(document, <ClientApp />);
2364
+ expect(Scheduler).toFlushWithoutYielding();
2365
expect(getMeaningfulChildren(document)).toEqual(
2366
<html>
2367
<head>
2462
- <link rel="stylesheet" href="qux" data-precedence="qux" />
2463
- <link rel="stylesheet" href="foo" data-precedence="foo" />
2368
+ <link rel="preload" as="style" href="foo" />
2369
+ <link rel="preload" as="font" href="baz" crossorigin="" />
2370
+ <link rel="preload" as="style" href="qux" />
2371
</head>
2465
- <body>client</body>
2466
- </html>,
2467
- );
2468
- });
2469
-
2470
- // @gate enableFloat && enableHostSingletons
2471
- it('retains styles in head through head remounts', async () => {
2472
- const root = ReactDOMClient.createRoot(document);
2473
- root.render(
2474
- <html>
2475
- <head key={1} />
2372
<body>
2477
- <link rel="stylesheet" href="foo" precedence="foo" />
2478
- <link rel="stylesheet" href="bar" precedence="bar" />
2479
- {null}
2480
- hello
2373
+ <div>hello</div>
2374
+ <link rel="preload" as="script" href="bar" />
2375
</body>
2376
</html>,
2377
);
2484
- expect(Scheduler).toFlushWithoutYielding();
2378
+ });
2379
+
2380
+ // @gate enableFloat
2381
+ it('can seed connection props for stylesheet and script resources', async () => {
2382
+ function App() {
2383
+ ReactDOM.preload('foo', {
2384
+ as: 'style',
2385
+ crossOrigin: 'use-credentials',
2386
+ integrity: 'some hash',
2387
+ });
2388
+ return (
2389
+ <html>
2390
+ <body>
2391
+ <div>hello</div>
2392
+ <link rel="stylesheet" href="foo" precedence="default" />
2393
+ </body>
2394
+ </html>
2395
+ );
2396
+ }
2397
+
2398
+ await actIntoEmptyDocument(() => {
2399
+ const {pipe} = renderToPipeableStream(<App />);
2400
+ pipe(writable);
2401
+ });
2402
+
2403
expect(getMeaningfulChildren(document)).toEqual(
2404
<html>
2405
<head>
2488
- <link rel="stylesheet" href="foo" data-precedence="foo" />
2489
- <link rel="stylesheet" href="bar" data-precedence="bar" />
2406
+ <link
2407
+ rel="stylesheet"
2408
+ href="foo"
2409
+ data-precedence="default"
2410
+ crossorigin="use-credentials"
2411
+ integrity="some hash"
2412
+ />
2413
</head>
2491
- <body>hello</body>
2492
- </html>,
2493
- );
2494
-
2495
- root.render(
2496
- <html>
2497
- <head key={2} />
2414
<body>
2499
- <link rel="stylesheet" href="foo" precedence="foo" />
2500
- {null}
2501
- <link rel="stylesheet" href="baz" precedence="baz" />
2502
- hello
2415
+ <div>hello</div>
2416
</body>
2417
</html>,
2418
);
2506
- expect(Scheduler).toFlushWithoutYielding();
2507
- // The reason we do not see preloads in the head is they are inserted synchronously
2508
- // during render and then when the new singleton mounts it resets it's content, retaining only styles
2509
- expect(getMeaningfulChildren(document)).toEqual(
2510
- <html>
2511
- <head>
2512
- <link rel="stylesheet" href="foo" data-precedence="foo" />
2513
- <link rel="stylesheet" href="bar" data-precedence="bar" />
2514
- <link rel="stylesheet" href="baz" data-precedence="baz" />
2515
- <link rel="preload" href="baz" as="style" />
2516
- </head>
2517
- <body>hello</body>
2518
- </html>,
2519
- );
2419
+ });
2420
+
2421
+ // @gate enableFloat
2422
+ it('warns if you do not pass in a valid href argument or options argument', async () => {
2423
+ function App() {
2424
+ ReactDOM.preload();
2425
+ ReactDOM.preload('');
2426
+ ReactDOM.preload('foo', null);
2427
+ ReactDOM.preload('foo', {});
2428
+ ReactDOM.preload('foo', {as: 'foo'});
2429
+ return <div>foo</div>;
2430
+ }
2431
+
2432
+ await expect(async () => {
2433
+ await act(() => {
2434
+ renderToPipeableStream(<App />).pipe(writable);
2435
+ });
2436
+ }).toErrorDev([
2437
+ 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead.',
2438
+ 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead.',
2439
+ 'ReactDOM.preload(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preloaded but encountered `null` instead.',
2440
+ 'ReactDOM.preload(): Expected the `as` property in the `options` argument (second) to contain a string value describing the type of resource to be preloaded but encountered `undefined` instead. Values that are valid in for the `as` attribute of a `<link rel="preload" as="..." />` tag are valid here.',
2441
+ ]);
2442
+ });
2443
+
2444
+ // @gate enableFloat
2445
+ it('warns if you pass incompatible options to two `ReactDOM.preload(...)` with the same href', async () => {
2446
+ function Component() {
2447
+ ReactDOM.preload('foo', {
2448
+ as: 'font',
2449
+ crossOrigin: 'use-credentials',
2450
+ });
2451
+ ReactDOM.preload('foo', {
2452
+ as: 'font',
2453
+ integrity: 'some hash',
2454
+ crossOrigin: 'anonymous',
2455
+ });
2456
+ ReactDOM.preload('foo', {
2457
+ as: 'font',
2458
+ extra: 'ignored',
2459
+ });
2460
+ }
2461
+
2462
+ await expect(async () => {
2463
+ await actIntoEmptyDocument(() => {
2464
+ renderToPipeableStream(
2465
+ <html>
2466
+ <body>
2467
+ <Component />
2468
+ </body>
2469
+ </html>,
2470
+ );
2471
+ });
2472
+ }).toErrorDev([
2473
+ 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2474
+ 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "crossOrigin" missing from options, original option value: "use-credentials"',
2475
+ ]);
2476
+ });
2477
+
2478
+ // @gate enableFloat
2479
+ it('warns if you pass incompatible options to two `ReactDOM.preload(...)` when an implicit preload already exists with the same href', async () => {
2480
+ function Component() {
2481
+ ReactDOM.preload('foo', {
2482
+ as: 'style',
2483
+ crossOrigin: 'use-credentials',
2484
+ });
2485
+ }
2486
+
2487
+ await expect(async () => {
2488
+ await actIntoEmptyDocument(() => {
2489
+ renderToPipeableStream(
2490
+ <html>
2491
+ <body>
2492
+ <link
2493
+ rel="stylesheet"
2494
+ href="foo"
2495
+ integrity="some hash"
2496
+ media="print"
2497
+ />
2498
+ <Component />
2499
+ </body>
2500
+ </html>,
2501
+ );
2502
+ });
2503
+ }).toErrorDev([
2504
+ 'ReactDOM.preload(): For `href` "foo", The options provided conflict with props on a matching <link rel="stylesheet" ... /> element. When the preload options disagree with the underlying resource it usually means the browser will not be able to use the preload when the resource is fetched, negating any benefit the preload would provide. React will preload the resource using props derived from the resource instead and ignore the options provided to the `ReactDOM.preload()` call. In general, preloading is useful when you expect to render a resource soon but have not yet done so. In this case since the underlying resource was already rendered the preload call may be extraneous. Try removing the call, otherwise try adjusting both the props on the <link rel="stylesheet" ... /> and the options passed to `ReactDOM.preload()` to agree.\n "integrity" missing from options, underlying prop value: "some hash"\n "media" missing from options, underlying prop value: "print"\n "crossOrigin" option value: "use-credentials", missing from underlying props',
2505
+ ]);
2506
});
2507
});
2508
2523
- describe('script resources', () => {
2509
+ describe('ReactDOM.preinit(href, { as: ... })', () => {
2510
// @gate enableFloat
2525
- it('treats async scripts without onLoad or onError as Resources', async () => {
2526
- await actIntoEmptyDocument(() => {
2527
- const {pipe} = renderToPipeableStream(
2511
+ it('creates a stylesheet resource when ReactDOM.preinit(..., {as: "style" }) is called', async () => {
2512
+ function App() {
2513
+ ReactDOM.preinit('foo', {as: 'style'});
2514
+ return (
2515
<html>
2529
- <head />
2516
<body>
2531
- <script src="foo" async={true} />
2532
- <script src="bar" async={true} onLoad={() => {}} />
2533
- <script src="baz" data-meaningful="" />
2534
- hello world
2517
+ <Suspense fallback="loading...">
2518
+ <BlockedOn value="bar">
2519
+ <Component />
2520
+ </BlockedOn>
2521
+ </Suspense>
2522
</body>
2536
- </html>,
2523
+ </html>
2524
);
2525
+ }
2526
+
2527
+ function Component() {
2528
+ ReactDOM.preinit('bar', {as: 'style'});
2529
+ return <div>hello</div>;
2530
+ }
2531
+
2532
+ await actIntoEmptyDocument(() => {
2533
+ const {pipe} = renderToPipeableStream(<App />);
2534
pipe(writable);
2535
});
2540
- // The plain async script is converted to a resource and emitted as part of the shell
2541
- // The async script with onLoad is preloaded in the shell but is expecting to be added
2542
- // during hydration. This is novel, the script is NOT a HostResource but it also will
2543
- // never hydrate
2544
- // The regular script is just a normal html that should hydrate with a HostComponent
2536
expect(getMeaningfulChildren(document)).toEqual(
2537
<html>
2538
<head>
2548
- <script src="foo" async="" />
2549
- <link rel="preload" href="bar" as="script" />
2539
+ <link rel="stylesheet" href="foo" data-precedence="default" />
2540
</head>
2551
- <body>
2552
- <script src="baz" data-meaningful="" />
2553
- hello world
2554
- </body>
2541
+ <body>loading...</body>
2542
</html>,
2543
);
2544
2558
- ReactDOMClient.hydrateRoot(
2559
- document,
2545
+ await act(() => {
2546
+ resolveText('bar');
2547
+ });
2548
+ // The reason we do not see the "bar" stylesheet here is that ReactDOM.preinit is not about
2549
+ // encoding a resource dependency but is a hint that a resource will be used in the near future.
2550
+ // If we call preinit on the server after the shell has flushed the best we can do is emit a preload
2551
+ // because any flushing suspense boundaries are not actually dependent on that resource and we don't
2552
+ // want to delay reveal based on when that resource loads.
2553
+ expect(getMeaningfulChildren(document)).toEqual(
2554
<html>
2561
- <head />
2555
+ <head>
2556
+ <link rel="stylesheet" href="foo" data-precedence="default" />
2557
+ </head>
2558
<body>
2563
- <script src="foo" async={true} />
2564
- <script src="bar" async={true} onLoad={() => {}} />
2565
- <script src="baz" data-meaningful="" />
2566
- hello world
2559
+ <div>hello</div>
2560
+ <link rel="preload" href="bar" as="style" />
2561
</body>
2562
</html>,
2563
);
2564
+
2565
+ function ClientApp() {
2566
+ ReactDOM.preinit('bar', {as: 'style'});
2567
+ return (
2568
+ <html>
2569
+ <body>
2570
+ <Suspense fallback="loading...">
2571
+ <div>hello</div>
2572
+ </Suspense>
2573
+ </body>
2574
+ </html>
2575
+ );
2576
+ }
2577
+
2578
+ ReactDOMClient.hydrateRoot(document, <ClientApp />);
2579
expect(Scheduler).toFlushWithoutYielding();
2571
- // The async script with onLoad is inserted in the right place but does not cause the hydration
2572
- // to fail.
2580
expect(getMeaningfulChildren(document)).toEqual(
2581
<html>
2582
<head>
2576
- <script src="foo" async="" />
2577
- <link rel="preload" href="bar" as="script" />
2583
+ <link rel="stylesheet" href="foo" data-precedence="default" />
2584
+ <link rel="stylesheet" href="bar" data-precedence="default" />
2585
</head>
2586
<body>
2580
- <script src="bar" async="" />
2581
- <script src="baz" data-meaningful="" />
2582
- hello world
2587
+ <div>hello</div>
2588
+ <link rel="preload" href="bar" as="style" />
2589
</body>
2590
</html>,
2591
);
2592
});
2587
- });
2593
2589
- // @gate enableFloat
2590
- it('client renders a boundary if a style Resource dependency fails to load', async () => {
2591
- function BlockedOn({text, children}) {
2592
- readText(text);
2593
- return children;
2594
- }
2595
- function App() {
2596
- return (
2594
+ // @gate enableFloat
2595
+ it('creates a preload resource when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
2596
+ function App() {
2597
+ React.useEffect(() => {
2598
+ ReactDOM.preinit('foo', {as: 'style'});
2599
+ }, []);
2600
+ return (
2601
+ <html>
2602
+ <body>foo</body>
2603
+ </html>
2604
+ );
2605
+ }
2606
+
2607
+ const root = ReactDOMClient.createRoot(document);
2608
+ root.render(<App />);
2609
+ expect(Scheduler).toFlushWithoutYielding();
2610
+ expect(getMeaningfulChildren(document)).toEqual(
2611
<html>
2598
- <head />
2599
- <body>
2600
- <Suspense fallback="loading...">
2601
- <BlockedOn text="unblock">
2602
- <link rel="stylesheet" href="foo" precedence="arbitrary" />
2603
- <link rel="stylesheet" href="bar" precedence="arbitrary" />
2604
- Hello
2605
- </BlockedOn>
2606
- </Suspense>
2607
- </body>
2608
- </html>
2612
+ <head>
2613
+ <link rel="preload" href="foo" as="style" />
2614
+ </head>
2615
+ <body>foo</body>
2616
+ </html>,
2617
);
2610
- }
2611
- await actIntoEmptyDocument(() => {
2612
- const {pipe} = renderToPipeableStream(<App />);
2613
- pipe(writable);
2618
});
2619
2616
- await act(() => {
2617
- resolveText('unblock');
2618
- });
2620
+ // @gate enableFloat
2621
+ it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called', async () => {
2622
+ function App() {
2623
+ ReactDOM.preinit('foo', {as: 'script'});
2624
+ return (
2625
+ <html>
2626
+ <body>
2627
+ <Suspense fallback="loading...">
2628
+ <BlockedOn value="bar">
2629
+ <Component />
2630
+ </BlockedOn>
2631
+ </Suspense>
2632
+ </body>
2633
+ </html>
2634
+ );
2635
+ }
2636
2620
- expect(getMeaningfulChildren(document)).toEqual(
2621
- <html>
2622
- <head>
2623
- <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2624
- <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2625
- </head>
2626
- <body>
2627
- loading...
2628
- <link rel="preload" href="foo" as="style" />
2629
- <link rel="preload" href="bar" as="style" />
2630
- </body>
2631
- </html>,
2632
- );
2637
+ function Component() {
2638
+ ReactDOM.preinit('bar', {as: 'script'});
2639
+ return <div>hello</div>;
2640
+ }
2641
2634
- await act(() => {
2635
- const barLink = document.querySelector(
2636
- 'link[rel="stylesheet"][href="bar"]',
2642
+ await actIntoEmptyDocument(() => {
2643
+ const {pipe} = renderToPipeableStream(<App />);
2644
+ pipe(writable);
2645
+ });
2646
+ expect(getMeaningfulChildren(document)).toEqual(
2647
+ <html>
2648
+ <head>
2649
+ <script async="" src="foo" />
2650
+ </head>
2651
+ <body>loading...</body>
2652
+ </html>,
2653
);
2638
- const event = document.createEvent('Events');
2639
- event.initEvent('error', true, true);
2640
- barLink.dispatchEvent(event);
2641
- });
2642
-
2643
- const boundaryTemplateInstance = document.getElementById('B:0');
2644
- const suspenseInstance = boundaryTemplateInstance.previousSibling;
2645
-
2646
- expect(suspenseInstance.data).toEqual('$!');
2647
- expect(boundaryTemplateInstance.dataset.dgst).toBe(
2648
- 'Resource failed to load',
2649
- );
2650
-
2651
- expect(getMeaningfulChildren(document)).toEqual(
2652
- <html>
2653
- <head>
2654
- <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2655
- <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2656
- </head>
2657
- <body>
2658
- loading...
2659
- <link rel="preload" href="foo" as="style" />
2660
- <link rel="preload" href="bar" as="style" />
2661
- </body>
2662
- </html>,
2663
- );
2664
-
2665
- const errors = [];
2666
- ReactDOMClient.hydrateRoot(document, <App />, {
2667
- onRecoverableError(err, errInfo) {
2668
- errors.push(err.message);
2669
- errors.push(err.digest);
2670
- },
2671
- });
2672
- expect(Scheduler).toFlushWithoutYielding();
2673
- expect(getMeaningfulChildren(document)).toEqual(
2674
- <html>
2675
- <head>
2676
- <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2677
- <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2678
- </head>
2679
- <body>
2680
- <link rel="preload" href="foo" as="style" />
2681
- <link rel="preload" href="bar" as="style" />
2682
- Hello
2683
- </body>
2684
- </html>,
2685
- );
2686
- expect(errors).toEqual([
2687
- 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
2688
- 'Resource failed to load',
2689
- ]);
2690
- });
2654
2692
- // @gate enableFloat
2693
- it('treats stylesheet links with a precedence as a resource', async () => {
2694
- await actIntoEmptyDocument(() => {
2695
- const {pipe} = renderToPipeableStream(
2655
+ await act(() => {
2656
+ resolveText('bar');
2657
+ });
2658
+ expect(getMeaningfulChildren(document)).toEqual(
2659
<html>
2697
- <head />
2660
+ <head>
2661
+ <script async="" src="foo" />
2662
+ </head>
2663
<body>
2699
- <link rel="stylesheet" href="foo" precedence="arbitrary" />
2700
- Hello
2664
+ <div>hello</div>
2665
+ <script async="" src="bar" />
2666
</body>
2702
- </html>,
2703
- );
2704
- pipe(writable);
2705
- });
2706
- expect(getMeaningfulChildren(document)).toEqual(
2707
- <html>
2708
- <head>
2709
- <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2710
- </head>
2711
- <body>Hello</body>
2712
- </html>,
2713
- );
2714
-
2715
- ReactDOMClient.hydrateRoot(
2716
- document,
2717
- <html>
2718
- <head />
2719
- <body>Hello</body>
2720
- </html>,
2721
- );
2722
- expect(Scheduler).toFlushWithoutYielding();
2723
- expect(getMeaningfulChildren(document)).toEqual(
2724
- <html>
2725
- <head>
2726
- <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2727
- </head>
2728
- <body>Hello</body>
2729
- </html>,
2730
- );
2731
- });
2732
-
2733
- // @gate enableFloat
2734
- 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 () => {
2735
- // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
2736
- // because the link gets elided as a resource
2737
- function AsyncTextWithResource({text, href, precedence}) {
2738
- const value = readText(text);
2739
- return (
2740
- <>
2741
- {value}
2742
- <link rel="stylesheet" href={href} precedence={precedence} />
2743
- </>
2667
+ </html>,
2668
);
2745
- }
2669
2747
- await actIntoEmptyDocument(() => {
2748
- const {pipe} = renderToPipeableStream(
2670
+ function ClientApp() {
2671
+ ReactDOM.preinit('bar', {as: 'script'});
2672
+ return (
2673
+ <html>
2674
+ <body>
2675
+ <Suspense fallback="loading...">
2676
+ <div>hello</div>
2677
+ </Suspense>
2678
+ </body>
2679
+ </html>
2680
+ );
2681
+ }
2682
+
2683
+ ReactDOMClient.hydrateRoot(document, <ClientApp />);
2684
+ expect(Scheduler).toFlushWithoutYielding();
2685
+ expect(getMeaningfulChildren(document)).toEqual(
2686
<html>
2750
- <head />
2687
+ <head>
2688
+ <script async="" src="foo" />
2689
+ </head>
2690
<body>
2752
- <AsyncTextWithResource text="foo" href="foo" precedence="one" />
2753
- <AsyncTextWithResource text="bar" href="bar" precedence="two" />
2754
- <AsyncTextWithResource text="baz" href="baz" precedence="three" />
2691
+ <div>hello</div>
2692
+ <script async="" src="bar" />
2693
</body>
2694
</html>,
2695
);
2758
- pipe(writable);
2759
- resolveText('foo');
2760
- resolveText('bar');
2761
- resolveText('baz');
2696
});
2697
2764
- expect(getMeaningfulChildren(document)).toEqual(
2765
- <html>
2766
- <head>
2767
- <link rel="stylesheet" href="foo" data-precedence="one" />
2768
- <link rel="stylesheet" href="bar" data-precedence="two" />
2769
- <link rel="stylesheet" href="baz" data-precedence="three" />
2770
- </head>
2771
- <body>
2772
- {'foo'}
2773
- {'bar'}
2774
- {'baz'}
2775
- </body>
2776
- </html>,
2777
- );
2778
- });
2698
+ // @gate enableFloat
2699
+ it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
2700
+ function App() {
2701
+ React.useEffect(() => {
2702
+ ReactDOM.preinit('foo', {as: 'script'});
2703
+ }, []);
2704
+ return (
2705
+ <html>
2706
+ <body>foo</body>
2707
+ </html>
2708
+ );
2709
+ }
2710
2780
- // @gate enableFloat
2781
- it('hoists late stylesheets the correct precedence', async () => {
2782
- function AsyncListItemWithResource({text, href, precedence, ...rest}) {
2783
- const value = readText(text);
2784
- return (
2785
- <li>
2786
- <link
2787
- rel="stylesheet"
2788
- href={href}
2789
- precedence={precedence}
2790
- {...rest}
2791
- />
2792
- {value}
2793
- </li>
2794
- );
2795
- }
2796
- function BlockingChildren({text, children}) {
2797
- readText(text);
2798
- return children;
2799
- }
2800
- function PresetPrecedence() {
2801
- ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
2802
- }
2803
- await actIntoEmptyDocument(() => {
2804
- const {pipe} = renderToPipeableStream(
2711
+ const root = ReactDOMClient.createRoot(document);
2712
+ root.render(<App />);
2713
+ expect(Scheduler).toFlushWithoutYielding();
2714
+ expect(getMeaningfulChildren(document)).toEqual(
2715
<html>
2806
- <head />
2807
- <body>
2808
- <link rel="stylesheet" href="initial" precedence="one" />
2809
- <PresetPrecedence />
2810
- <div>
2811
- <Suspense fallback="loading foo bar...">
2812
- <link rel="stylesheet" href="foo" precedence="one" />
2813
- <ul>
2814
- <li>
2815
- <AsyncText text="foo" />
2816
- </li>
2817
- <AsyncListItemWithResource
2818
- text="bar"
2819
- href="bar"
2820
- precedence="default"
2821
- data-foo="foo"
2822
- crossOrigin="anonymous"
2823
- />
2824
- </ul>
2825
- </Suspense>
2826
- </div>
2827
- <div>
2828
- <Suspense fallback="loading bar baz qux...">
2829
- <ul>
2830
- <AsyncListItemWithResource
2831
- text="bar"
2832
- href="bar"
2833
- precedence="default"
2834
- />
2835
- <AsyncListItemWithResource
2836
- text="baz"
2837
- href="baz"
2838
- precedence="two"
2839
- />
2840
- <AsyncListItemWithResource
2841
- text="qux"
2842
- href="qux"
2843
- precedence="one"
2844
- />
2845
- </ul>
2846
- </Suspense>
2847
- </div>
2848
- <div>
2849
- <Suspense fallback="loading bar baz qux...">
2850
- <BlockingChildren text="unblock">
2851
- <ul>
2852
- <AsyncListItemWithResource
2853
- text="bar"
2854
- href="bar"
2855
- precedence="default"
2856
- />
2857
- <AsyncListItemWithResource
2858
- text="baz"
2859
- href="baz"
2860
- precedence="two"
2861
- />
2862
- <AsyncListItemWithResource
2863
- text="qux"
2864
- href="qux"
2865
- precedence="one"
2866
- />
2867
- </ul>
2868
- </BlockingChildren>
2869
- </Suspense>
2870
- </div>
2871
- </body>
2716
+ <head>
2717
+ <link rel="preload" href="foo" as="script" />
2718
+ </head>
2719
+ <body>foo</body>
2720
</html>,
2721
);
2874
- pipe(writable);
2722
});
2723
2877
- expect(getMeaningfulChildren(document)).toEqual(
2878
- <html>
2879
- <head>
2880
- <link rel="stylesheet" href="initial" data-precedence="one" />
2881
- <link rel="stylesheet" href="preset" data-precedence="preset" />
2882
- <link rel="preload" href="foo" as="style" />
2883
- </head>
2884
- <body>
2885
- <div>loading foo bar...</div>
2886
- <div>loading bar baz qux...</div>
2887
- <div>loading bar baz qux...</div>
2888
- </body>
2889
- </html>,
2890
- );
2724
+ // @gate enableFloat
2725
+ it('warns if you do not pass in a valid href argument or options argument', async () => {
2726
+ function App() {
2727
+ ReactDOM.preinit();
2728
+ ReactDOM.preinit('');
2729
+ ReactDOM.preinit('foo', null);
2730
+ ReactDOM.preinit('foo', {});
2731
+ ReactDOM.preinit('foo', {as: 'foo'});
2732
+ return <div>foo</div>;
2733
+ }
2734
2892
- await act(() => {
2893
- resolveText('foo');
2894
- resolveText('bar');
2735
+ await expect(async () => {
2736
+ await act(() => {
2737
+ renderToPipeableStream(<App />).pipe(writable);
2738
+ });
2739
+ }).toErrorDev([
2740
+ 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead',
2741
+ 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead',
2742
+ 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered `null` instead',
2743
+ 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered `undefined` instead. Valid values for `as` are "style" and "script".',
2744
+ 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered "foo" instead. Valid values for `as` are "style" and "script".',
2745
+ ]);
2746
});
2747
2897
- expect(getMeaningfulChildren(document)).toEqual(
2898
- <html>
2899
- <head>
2900
- <link rel="stylesheet" href="initial" data-precedence="one" />
2901
- <link rel="stylesheet" href="foo" data-precedence="one" />
2902
- <link rel="stylesheet" href="preset" data-precedence="preset" />
2903
- <link
2904
- rel="stylesheet"
2905
- href="bar"
2906
- data-precedence="default"
2907
- data-foo="foo"
2908
- crossorigin="anonymous"
2909
- />
2910
- <link rel="preload" href="foo" as="style" />
2911
- </head>
2912
- <body>
2913
- <div>loading foo bar...</div>
2914
- <div>loading bar baz qux...</div>
2915
- <div>loading bar baz qux...</div>
2916
- <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2917
- </body>
2918
- </html>,
2919
- );
2748
+ // @gate enableFloat
2749
+ it('warns if you pass options to `ReactDOM.preinit(..., { as: "style", ... })` incompatible with props from an existing <link rel="stylesheet" .../>', async () => {
2750
+ function Component() {
2751
+ ReactDOM.preinit('foo', {
2752
+ as: 'style',
2753
+ integrity: 'some hash',
2754
+ crossOrigin: 'use-credentials',
2755
+ });
2756
+ }
2757
2921
- await act(() => {
2922
- const link = document.querySelector('link[rel="stylesheet"][href="foo"]');
2923
- const event = document.createEvent('Events');
2924
- event.initEvent('load', true, true);
2925
- link.dispatchEvent(event);
2758
+ await expect(async () => {
2759
+ await actIntoEmptyDocument(() => {
2760
+ renderToPipeableStream(
2761
+ <html>
2762
+ <body>
2763
+ <link
2764
+ rel="stylesheet"
2765
+ href="foo"
2766
+ precedence="foo"
2767
+ crossOrigin="anonymous"
2768
+ />
2769
+ <Component />
2770
+ </body>
2771
+ </html>,
2772
+ );
2773
+ });
2774
+ }).toErrorDev([
2775
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a <link rel="stylesheet" precedence="foo" href="foo" .../> that was already rendered. React will always use the props or options it first encounters for a hoistable stylesheet for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a stylesheet but you anticipate it will be used soon. In this case the stylesheet was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the <link rel="stylesheet" .../> and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "precedence" missing from options, prop value: "foo"\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
2776
+ ]);
2777
});
2778
2928
- expect(getMeaningfulChildren(document)).toEqual(
2929
- <html>
2930
- <head>
2931
- <link rel="stylesheet" href="initial" data-precedence="one" />
2932
- <link rel="stylesheet" href="foo" data-precedence="one" />
2933
- <link rel="stylesheet" href="preset" data-precedence="preset" />
2934
- <link
2935
- rel="stylesheet"
2936
- href="bar"
2937
- data-precedence="default"
2938
- data-foo="foo"
2939
- crossorigin="anonymous"
2940
- />
2941
- <link rel="preload" href="foo" as="style" />
2942
- </head>
2943
- <body>
2944
- <div>loading foo bar...</div>
2945
- <div>loading bar baz qux...</div>
2946
- <div>loading bar baz qux...</div>
2947
- <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2948
- </body>
2949
- </html>,
2950
- );
2779
+ // @gate enableFloat
2780
+ it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "style", ... })` with the same href', async () => {
2781
+ function Component() {
2782
+ ReactDOM.preinit('foo', {
2783
+ as: 'style',
2784
+ precedence: 'foo',
2785
+ crossOrigin: 'use-credentials',
2786
+ });
2787
+ ReactDOM.preinit('foo', {
2788
+ as: 'style',
2789
+ integrity: 'some hash',
2790
+ crossOrigin: 'anonymous',
2791
+ });
2792
+ }
2793
2952
- await act(() => {
2953
- const link = document.querySelector('link[rel="stylesheet"][href="bar"]');
2954
- const event = document.createEvent('Events');
2955
- event.initEvent('load', true, true);
2956
- link.dispatchEvent(event);
2794
+ await expect(async () => {
2795
+ await actIntoEmptyDocument(() => {
2796
+ renderToPipeableStream(
2797
+ <html>
2798
+ <body>
2799
+ <Component />
2800
+ </body>
2801
+ </html>,
2802
+ );
2803
+ });
2804
+ }).toErrorDev([
2805
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "style", ... })`. React will always use the options it first encounters when preinitializing a hoistable stylesheet for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "precedence" missing from options, original option value: "foo"\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2806
+ ]);
2807
});
2808
2959
- expect(getMeaningfulChildren(document)).toEqual(
2960
- <html>
2961
- <head>
2962
- <link rel="stylesheet" href="initial" data-precedence="one" />
2963
- <link rel="stylesheet" href="foo" data-precedence="one" />
2964
- <link rel="stylesheet" href="preset" data-precedence="preset" />
2965
- <link
2966
- rel="stylesheet"
2967
- href="bar"
2968
- data-precedence="default"
2969
- data-foo="foo"
2970
- crossorigin="anonymous"
2971
- />
2972
- <link rel="preload" href="foo" as="style" />
2973
- </head>
2974
- <body>
2975
- <div>
2976
- <ul>
2977
- <li>foo</li>
2978
- <li>bar</li>
2979
- </ul>
2980
- </div>
2981
- <div>loading bar baz qux...</div>
2982
- <div>loading bar baz qux...</div>
2983
- <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2984
- </body>
2985
- </html>,
2986
- );
2809
+ // @gate enableFloat
2810
+ it('warns if you pass options to `ReactDOM.preinit(..., { as: "script", ... })` incompatible with props from an existing <script async={true} .../>', async () => {
2811
+ function Component() {
2812
+ ReactDOM.preinit('foo', {
2813
+ as: 'script',
2814
+ integrity: 'some hash',
2815
+ crossOrigin: 'use-credentials',
2816
+ });
2817
+ }
2818
+
2819
+ await expect(async () => {
2820
+ await actIntoEmptyDocument(() => {
2821
+ renderToPipeableStream(
2822
+ <html>
2823
+ <body>
2824
+ <script async={true} src="foo" crossOrigin="anonymous" />
2825
+ <Component />
2826
+ </body>
2827
+ </html>,
2828
+ );
2829
+ });
2830
+ }).toErrorDev([
2831
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a <script async={true} src="foo" .../> that was already rendered. React will always use the props or options it first encounters for a hoistable script for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a script but you anticipate it will be used soon and want to go beyond preloading it and have it execute early. In this case the script was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the <script .../> and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
2832
+ ]);
2833
+ });
2834
2988
- await act(() => {
2989
- resolveText('baz');
2835
+ // @gate enableFloat
2836
+ it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "script", ... })` with the same href', async () => {
2837
+ function Component() {
2838
+ ReactDOM.preinit('foo', {
2839
+ as: 'script',
2840
+ crossOrigin: 'use-credentials',
2841
+ });
2842
+ ReactDOM.preinit('foo', {
2843
+ as: 'script',
2844
+ integrity: 'some hash',
2845
+ crossOrigin: 'anonymous',
2846
+ });
2847
+ }
2848
+
2849
+ await expect(async () => {
2850
+ await actIntoEmptyDocument(() => {
2851
+ renderToPipeableStream(
2852
+ <html>
2853
+ <body>
2854
+ <Component />
2855
+ </body>
2856
+ </html>,
2857
+ );
2858
+ });
2859
+ }).toErrorDev([
2860
+ 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "script", ... })`. React will always use the options it first encounters when preinitializing a hoistable script for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2861
+ ]);
2862
});
2863
+ });
2864
2992
- expect(getMeaningfulChildren(document)).toEqual(
2993
- <html>
2994
- <head>
2995
- <link rel="stylesheet" href="initial" data-precedence="one" />
2996
- <link rel="stylesheet" href="foo" data-precedence="one" />
2997
- <link rel="stylesheet" href="preset" data-precedence="preset" />
2998
- <link
2999
- rel="stylesheet"
3000
- href="bar"
3001
- data-precedence="default"
3002
- data-foo="foo"
3003
- crossorigin="anonymous"
3004
- />
3005
- <link rel="preload" href="foo" as="style" />
3006
- </head>
3007
- <body>
3008
- <div>
3009
- <ul>
3010
- <li>foo</li>
3011
- <li>bar</li>
3012
- </ul>
3013
- </div>
3014
- <div>loading bar baz qux...</div>
3015
- <div>loading bar baz qux...</div>
3016
- <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3017
- <link rel="preload" as="style" href="baz" />
3018
- </body>
3019
- </html>,
3020
- );
2865
+ describe('Stylesheet Resources', () => {
2866
+ // @gate enableFloat
2867
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when server rendering', async () => {
2868
+ await actIntoEmptyDocument(() => {
2869
+ const {pipe} = renderToPipeableStream(
2870
+ <html>
2871
+ <head />
2872
+ <body>
2873
+ <link rel="stylesheet" href="aresource" precedence="foo" />
2874
+ <div>hello world</div>
2875
+ </body>
2876
+ </html>,
2877
+ );
2878
+ pipe(writable);
2879
+ });
2880
3022
- await act(() => {
3023
- resolveText('qux');
2881
+ expect(getMeaningfulChildren(document)).toEqual(
2882
+ <html>
2883
+ <head>
2884
+ <link rel="stylesheet" href="aresource" data-precedence="foo" />
2885
+ </head>
2886
+ <body>
2887
+ <div>hello world</div>
2888
+ </body>
2889
+ </html>,
2890
+ );
2891
});
2892
3026
- expect(getMeaningfulChildren(document)).toEqual(
3027
- <html>
3028
- <head>
3029
- <link rel="stylesheet" href="initial" data-precedence="one" />
3030
- <link rel="stylesheet" href="foo" data-precedence="one" />
3031
- <link rel="stylesheet" href="qux" data-precedence="one" />
3032
- <link rel="stylesheet" href="preset" data-precedence="preset" />
3033
- <link
3034
- rel="stylesheet"
3035
- href="bar"
3036
- data-precedence="default"
3037
- data-foo="foo"
3038
- crossorigin="anonymous"
3039
- />
3040
- <link rel="stylesheet" href="baz" data-precedence="two" />
3041
- <link rel="preload" href="foo" as="style" />
3042
- </head>
3043
- <body>
3044
- <div>
3045
- <ul>
3046
- <li>foo</li>
3047
- <li>bar</li>
3048
- </ul>
3049
- </div>
3050
- <div>loading bar baz qux...</div>
3051
- <div>loading bar baz qux...</div>
3052
- <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3053
- <link rel="preload" as="style" href="baz" />
3054
- <link rel="preload" as="style" href="qux" />
3055
- </body>
3056
- </html>,
3057
- );
2893
+ // @gate enableFloat
2894
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when client rendering', async () => {
2895
+ const root = ReactDOMClient.createRoot(document);
2896
+ root.render(
2897
+ <html>
2898
+ <head />
2899
+ <body>
2900
+ <link rel="stylesheet" href="aresource" precedence="foo" />
2901
+ <div>hello world</div>
2902
+ </body>
2903
+ </html>,
2904
+ );
2905
+ expect(Scheduler).toFlushWithoutYielding();
2906
3059
- await act(() => {
3060
- const bazlink = document.querySelector(
3061
- 'link[rel="stylesheet"][href="baz"]',
2907
+ expect(getMeaningfulChildren(document)).toEqual(
2908
+ <html>
2909
+ <head>
2910
+ <link rel="stylesheet" href="aresource" data-precedence="foo" />
2911
+ </head>
2912
+ <body>
2913
+ <div>hello world</div>
2914
+ </body>
2915
+ </html>,
2916
);
3063
- const quxlink = document.querySelector(
3064
- 'link[rel="stylesheet"][href="qux"]',
2917
+ });
2918
+
2919
+ // @gate enableFloat
2920
+ it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when hydrating', async () => {
2921
+ await actIntoEmptyDocument(() => {
2922
+ const {pipe} = renderToPipeableStream(
2923
+ <html>
2924
+ <head />
2925
+ <body>
2926
+ <link rel="stylesheet" href="aresource" precedence="foo" />
2927
+ <div>hello world</div>
2928
+ </body>
2929
+ </html>,
2930
+ );
2931
+ pipe(writable);
2932
+ });
2933
+ ReactDOMClient.hydrateRoot(
2934
+ document,
2935
+ <html>
2936
+ <head />
2937
+ <body>
2938
+ <link rel="stylesheet" href="aresource" precedence="foo" />
2939
+ <div>hello world</div>
2940
+ </body>
2941
+ </html>,
2942
);
3066
- const presetLink = document.querySelector(
3067
- 'link[rel="stylesheet"][href="preset"]',
2943
+ expect(Scheduler).toFlushWithoutYielding();
2944
+
2945
+ expect(getMeaningfulChildren(document)).toEqual(
2946
+ <html>
2947
+ <head>
2948
+ <link rel="stylesheet" href="aresource" data-precedence="foo" />
2949
+ </head>
2950
+ <body>
2951
+ <div>hello world</div>
2952
+ </body>
2953
+ </html>,
2954
);
3069
- const event = document.createEvent('Events');
3070
- event.initEvent('load', true, true);
3071
- bazlink.dispatchEvent(event);
3072
- quxlink.dispatchEvent(event);
3073
- presetLink.dispatchEvent(event);
2955
});
2956
3076
- expect(getMeaningfulChildren(document)).toEqual(
3077
- <html>
3078
- <head>
3079
- <link rel="stylesheet" href="initial" data-precedence="one" />
3080
- <link rel="stylesheet" href="foo" data-precedence="one" />
3081
- <link rel="stylesheet" href="qux" data-precedence="one" />
3082
- <link rel="stylesheet" href="preset" data-precedence="preset" />
3083
- <link
3084
- rel="stylesheet"
3085
- href="bar"
3086
- data-precedence="default"
3087
- data-foo="foo"
3088
- crossorigin="anonymous"
3089
- />
3090
- <link rel="stylesheet" href="baz" data-precedence="two" />
3091
- <link rel="preload" href="foo" as="style" />
3092
- </head>
3093
- <body>
3094
- <div>
3095
- <ul>
3096
- <li>foo</li>
3097
- <li>bar</li>
3098
- </ul>
3099
- </div>
3100
- <div>
3101
- <ul>
3102
- <li>bar</li>
3103
- <li>baz</li>
3104
- <li>qux</li>
3105
- </ul>
3106
- </div>
3107
- <div>loading bar baz qux...</div>
3108
- <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3109
- <link rel="preload" as="style" href="baz" />
3110
- <link rel="preload" as="style" href="qux" />
3111
- </body>
3112
- </html>,
3113
- );
2957
+ // @gate enableFloat
2958
+ it('preloads stylesheets without a precedence prop when server rendering', async () => {
2959
+ await actIntoEmptyDocument(() => {
2960
+ const {pipe} = renderToPipeableStream(
2961
+ <html>
2962
+ <head />
2963
+ <body>
2964
+ <link rel="stylesheet" href="notaresource" />
2965
+ <div>hello world</div>
2966
+ </body>
2967
+ </html>,
2968
+ );
2969
+ pipe(writable);
2970
+ });
2971
3115
- await act(() => {
3116
- resolveText('unblock');
2972
+ expect(getMeaningfulChildren(document)).toEqual(
2973
+ <html>
2974
+ <head>
2975
+ <link rel="preload" as="style" href="notaresource" />
2976
+ </head>
2977
+ <body>
2978
+ <link rel="stylesheet" href="notaresource" />
2979
+ <div>hello world</div>
2980
+ </body>
2981
+ </html>,
2982
+ );
2983
});
2984
3119
- expect(getMeaningfulChildren(document)).toEqual(
3120
- <html>
3121
- <head>
3122
- <link rel="stylesheet" href="initial" data-precedence="one" />
3123
- <link rel="stylesheet" href="foo" data-precedence="one" />
3124
- <link rel="stylesheet" href="qux" data-precedence="one" />
3125
- <link rel="stylesheet" href="preset" data-precedence="preset" />
3126
- <link
3127
- rel="stylesheet"
3128
- href="bar"
3129
- data-precedence="default"
3130
- data-foo="foo"
3131
- crossorigin="anonymous"
3132
- />
3133
- <link rel="stylesheet" href="baz" data-precedence="two" />
3134
- <link rel="preload" href="foo" as="style" />
3135
- </head>
3136
- <body>
3137
- <div>
3138
- <ul>
3139
- <li>foo</li>
3140
- <li>bar</li>
3141
- </ul>
3142
- </div>
3143
- <div>
3144
- <ul>
3145
- <li>bar</li>
3146
- <li>baz</li>
3147
- <li>qux</li>
3148
- </ul>
3149
- </div>
3150
- <div>
3151
- <ul>
3152
- <li>bar</li>
3153
- <li>baz</li>
3154
- <li>qux</li>
3155
- </ul>
3156
- </div>
3157
- <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3158
- <link rel="preload" as="style" href="baz" />
3159
- <link rel="preload" as="style" href="qux" />
3160
- </body>
3161
- </html>,
3162
- );
3163
- });
2985
+ // @gate enableFloat
2986
+ it('hoists stylesheet resources to the correct precedence', async () => {
2987
+ await actIntoEmptyDocument(() => {
2988
+ const {pipe} = renderToPipeableStream(
2989
+ <html>
2990
+ <head />
2991
+ <body>
2992
+ <link rel="stylesheet" href="foo1" precedence="foo" />
2993
+ <link rel="stylesheet" href="default1" precedence="default" />
2994
+ <link rel="stylesheet" href="foo2" precedence="foo" />
2995
+ <div>hello world</div>
2996
+ </body>
2997
+ </html>,
2998
+ );
2999
+ pipe(writable);
3000
+ });
3001
+ expect(getMeaningfulChildren(document)).toEqual(
3002
+ <html>
3003
+ <head>
3004
+ <link rel="stylesheet" href="foo1" data-precedence="foo" />
3005
+ <link rel="stylesheet" href="foo2" data-precedence="foo" />
3006
+ <link rel="stylesheet" href="default1" data-precedence="default" />
3007
+ </head>
3008
+ <body>
3009
+ <div>hello world</div>
3010
+ </body>
3011
+ </html>,
3012
+ );
3013
3165
- // @gate enableFloat
3166
- it('normalizes style resource precedence for all boundaries inlined as part of the shell flush', async () => {
3167
- await actIntoEmptyDocument(() => {
3168
- const {pipe} = renderToPipeableStream(
3014
+ ReactDOMClient.hydrateRoot(
3015
+ document,
3016
<html>
3017
<head />
3018
<body>
3172
- <div>
3173
- outer
3174
- <link rel="stylesheet" href="1one" precedence="one" />
3175
- <link rel="stylesheet" href="1two" precedence="two" />
3176
- <link rel="stylesheet" href="1three" precedence="three" />
3177
- <link rel="stylesheet" href="1four" precedence="four" />
3178
- <Suspense fallback={null}>
3179
- <div>
3180
- middle
3181
- <link rel="stylesheet" href="2one" precedence="one" />
3182
- <link rel="stylesheet" href="2two" precedence="two" />
3183
- <link rel="stylesheet" href="2three" precedence="three" />
3184
- <link rel="stylesheet" href="2four" precedence="four" />
3185
- <Suspense fallback={null}>
3186
- <div>
3187
- inner
3188
- <link rel="stylesheet" href="3five" precedence="five" />
3189
- <link rel="stylesheet" href="3one" precedence="one" />
3190
- <link rel="stylesheet" href="3two" precedence="two" />
3191
- <link rel="stylesheet" href="3three" precedence="three" />
3192
- <link rel="stylesheet" href="3four" precedence="four" />
3193
- </div>
3194
- </Suspense>
3195
- </div>
3196
- </Suspense>
3197
- <Suspense fallback={null}>
3198
- <div>middle</div>
3199
- <link rel="stylesheet" href="4one" precedence="one" />
3200
- <link rel="stylesheet" href="4two" precedence="two" />
3201
- <link rel="stylesheet" href="4three" precedence="three" />
3202
- <link rel="stylesheet" href="4four" precedence="four" />
3203
- </Suspense>
3204
- </div>
3019
+ <link rel="stylesheet" href="bar1" precedence="bar" />
3020
+ <link rel="stylesheet" href="foo3" precedence="foo" />
3021
+ <link rel="stylesheet" href="default2" precedence="default" />
3022
+ <div>hello world</div>
3023
+ </body>
3024
+ </html>,
3025
+ );
3026
+ expect(Scheduler).toFlushWithoutYielding();
3027
+ expect(getMeaningfulChildren(document)).toEqual(
3028
+ <html>
3029
+ <head>
3030
+ <link rel="stylesheet" href="foo1" data-precedence="foo" />
3031
+ <link rel="stylesheet" href="foo2" data-precedence="foo" />
3032
+ <link rel="stylesheet" href="foo3" data-precedence="foo" />
3033
+ <link rel="stylesheet" href="default1" data-precedence="default" />
3034
+ <link rel="stylesheet" href="default2" data-precedence="default" />
3035
+ <link rel="stylesheet" href="bar1" data-precedence="bar" />
3036
+ <link rel="preload" as="style" href="bar1" />
3037
+ <link rel="preload" as="style" href="foo3" />
3038
+ <link rel="preload" as="style" href="default2" />
3039
+ </head>
3040
+ <body>
3041
+ <div>hello world</div>
3042
</body>
3043
</html>,
3044
);
3208
- pipe(writable);
3045
});
3046
3211
- // The reason the href's aren't ordered linearly is that when boundaries complete their resources
3212
- // get hoisted to the shell directly so they can flush in the head. If a boundary doesn't suspend then
3213
- // child boundaries will complete before the parent boundary and thus have their resources hoist
3214
- // early. The reason precedences are still ordered correctly between child and parent is because
3215
- // the precedence ordering is determined upon first discovernig a resource rather than on hoist and
3216
- // so it follows render order
3217
- expect(getMeaningfulChildren(document)).toEqual(
3218
- <html>
3219
- <head>
3220
- <link rel="stylesheet" href="1one" data-precedence="one" />
3221
- <link rel="stylesheet" href="3one" data-precedence="one" />
3222
- <link rel="stylesheet" href="2one" data-precedence="one" />
3223
- <link rel="stylesheet" href="4one" data-precedence="one" />
3047
+ // @gate enableFloat
3048
+ it('retains styles even after the last referring Resource unmounts', async () => {
3049
+ // This test is true until a future update where there is some form of garbage collection.
3050
+ const root = ReactDOMClient.createRoot(document);
3051
3225
- <link rel="stylesheet" href="1two" data-precedence="two" />
3226
- <link rel="stylesheet" href="3two" data-precedence="two" />
3227
- <link rel="stylesheet" href="2two" data-precedence="two" />
3228
- <link rel="stylesheet" href="4two" data-precedence="two" />
3052
+ root.render(
3053
+ <html>
3054
+ <head />
3055
+ <body>
3056
+ hello world
3057
+ <link rel="stylesheet" href="foo" precedence="foo" />
3058
+ </body>
3059
+ </html>,
3060
+ );
3061
+ expect(Scheduler).toFlushWithoutYielding();
3062
3230
- <link rel="stylesheet" href="1three" data-precedence="three" />
3231
- <link rel="stylesheet" href="3three" data-precedence="three" />
3232
- <link rel="stylesheet" href="2three" data-precedence="three" />
3233
- <link rel="stylesheet" href="4three" data-precedence="three" />
3063
+ root.render(
3064
+ <html>
3065
+ <head />
3066
+ <body>hello world</body>
3067
+ </html>,
3068
+ );
3069
+ expect(Scheduler).toFlushWithoutYielding();
3070
+ expect(getMeaningfulChildren(document)).toEqual(
3071
+ <html>
3072
+ <head>
3073
+ <link rel="stylesheet" href="foo" data-precedence="foo" />
3074
+ </head>
3075
+ <body>hello world</body>
3076
+ </html>,
3077
+ );
3078
+ });
3079
3235
- <link rel="stylesheet" href="1four" data-precedence="four" />
3236
- <link rel="stylesheet" href="3four" data-precedence="four" />
3237
- <link rel="stylesheet" href="2four" data-precedence="four" />
3238
- <link rel="stylesheet" href="4four" data-precedence="four" />
3080
+ // @gate enableFloat && enableHostSingletons && enableClientRenderFallbackOnTextMismatch
3081
+ it('retains styles even when a new html, head, and/body mount', async () => {
3082
+ await actIntoEmptyDocument(() => {
3083
+ const {pipe} = renderToPipeableStream(
3084
+ <html>
3085
+ <head />
3086
+ <body>
3087
+ <link rel="stylesheet" href="foo" precedence="foo" />
3088
+ <link rel="stylesheet" href="bar" precedence="bar" />
3089
+ server
3090
+ </body>
3091
+ </html>,
3092
+ );
3093
+ pipe(writable);
3094
+ });
3095
+ const errors = [];
3096
+ ReactDOMClient.hydrateRoot(
3097
+ document,
3098
+ <html>
3099
+ <head>
3100
+ <link rel="stylesheet" href="qux" precedence="qux" />
3101
+ <link rel="stylesheet" href="foo" precedence="foo" />
3102
+ </head>
3103
+ <body>client</body>
3104
+ </html>,
3105
+ {
3106
+ onRecoverableError(error) {
3107
+ errors.push(error.message);
3108
+ },
3109
+ },
3110
+ );
3111
+ expect(() => {
3112
+ expect(Scheduler).toFlushWithoutYielding();
3113
+ }).toErrorDev(
3114
+ [
3115
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
This file is too large to show in full.