566
' in App (at **)',
567
]);
568
});
569
+
570
+ it('should render root host components into body scope when the container is a Document', async () => {
571
+ function App({phase}) {
572
+ return (
573
+ <>
574
+ {phase < 1 ? null : <div>..before</div>}
575
+ {phase < 3 ? <div>before</div> : null}
576
+ {phase < 2 ? null : <div>before..</div>}
577
+ <html lang="en">
578
+ <head data-h="">
579
+ {phase < 1 ? null : <meta itemProp="" content="..head" />}
580
+ {phase < 3 ? <meta itemProp="" content="head" /> : null}
581
+ {phase < 2 ? null : <meta itemProp="" content="head.." />}
582
+ </head>
583
+ <body data-b="">
584
+ {phase < 1 ? null : <div>..inside</div>}
585
+ {phase < 3 ? <div>inside</div> : null}
586
+ {phase < 2 ? null : <div>inside..</div>}
587
+ </body>
588
+ </html>
589
+ {phase < 1 ? null : <div>..after</div>}
590
+ {phase < 3 ? <div>after</div> : null}
591
+ {phase < 2 ? null : <div>after..</div>}
592
+ </>
593
+ );
594
+ }
595
+
596
+ const root = ReactDOMClient.createRoot(document);
597
+ await act(() => {
598
+ root.render(<App phase={0} />);
599
+ });
600
+ expect(document.documentElement.outerHTML).toBe(
601
+ '<html lang="en"><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
602
+ );
603
+
604
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
605
+ // root of the application
606
+ assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
607
+
608
+ await act(() => {
609
+ root.render(<App phase={1} />);
610
+ });
611
+ expect(document.documentElement.outerHTML).toBe(
612
+ '<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body data-b=""><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
613
+ );
614
+
615
+ await act(() => {
616
+ root.render(<App phase={2} />);
617
+ });
618
+ expect(document.documentElement.outerHTML).toBe(
619
+ '<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
620
+ );
621
+
622
+ await act(() => {
623
+ root.render(<App phase={3} />);
624
+ });
625
+ expect(document.documentElement.outerHTML).toBe(
626
+ '<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
627
+ );
628
+
629
+ await act(() => {
630
+ root.unmount();
631
+ });
632
+ expect(document.documentElement.outerHTML).toBe(
633
+ '<html><head></head><body></body></html>',
634
+ );
635
+ });
636
+
637
+ it('should render root host components into body scope when the container is a the <html> tag', async () => {
638
+ function App({phase}) {
639
+ return (
640
+ <>
641
+ {phase < 1 ? null : <div>..before</div>}
642
+ {phase < 3 ? <div>before</div> : null}
643
+ {phase < 2 ? null : <div>before..</div>}
644
+ <head data-h="">
645
+ {phase < 1 ? null : <meta itemProp="" content="..head" />}
646
+ {phase < 3 ? <meta itemProp="" content="head" /> : null}
647
+ {phase < 2 ? null : <meta itemProp="" content="head.." />}
648
+ </head>
649
+ <body data-b="">
650
+ {phase < 1 ? null : <div>..inside</div>}
651
+ {phase < 3 ? <div>inside</div> : null}
652
+ {phase < 2 ? null : <div>inside..</div>}
653
+ </body>
654
+ {phase < 1 ? null : <div>..after</div>}
655
+ {phase < 3 ? <div>after</div> : null}
656
+ {phase < 2 ? null : <div>after..</div>}
657
+ </>
658
+ );
659
+ }
660
+
661
+ const root = ReactDOMClient.createRoot(document.documentElement);
662
+ await act(() => {
663
+ root.render(<App phase={0} />);
664
+ });
665
+ expect(document.documentElement.outerHTML).toBe(
666
+ '<html><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
667
+ );
668
+
669
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
670
+ // root of the application
671
+ assertConsoleErrorDev(['In HTML, <div> cannot be a child of <html>']);
672
+
673
+ await act(() => {
674
+ root.render(<App phase={1} />);
675
+ });
676
+ expect(document.documentElement.outerHTML).toBe(
677
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body data-b=""><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
678
+ );
679
+
680
+ await act(() => {
681
+ root.render(<App phase={2} />);
682
+ });
683
+ expect(document.documentElement.outerHTML).toBe(
684
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
685
+ );
686
+
687
+ await act(() => {
688
+ root.render(<App phase={3} />);
689
+ });
690
+ expect(document.documentElement.outerHTML).toBe(
691
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
692
+ );
693
+
694
+ await act(() => {
695
+ root.unmount();
696
+ });
697
+ expect(document.documentElement.outerHTML).toBe(
698
+ '<html><head></head><body></body></html>',
699
+ );
700
+ });
701
+
702
+ it('should render root host components into body scope when the container is a the <body> tag', async () => {
703
+ function App({phase}) {
704
+ return (
705
+ <>
706
+ {phase < 1 ? null : <div>..before</div>}
707
+ {phase < 3 ? <div>before</div> : null}
708
+ {phase < 2 ? null : <div>before..</div>}
709
+ <head data-h="">
710
+ {phase < 1 ? null : <meta itemProp="" content="..head" />}
711
+ {phase < 3 ? <meta itemProp="" content="head" /> : null}
712
+ {phase < 2 ? null : <meta itemProp="" content="head.." />}
713
+ </head>
714
+ {phase < 1 ? null : <div>..inside</div>}
715
+ {phase < 3 ? <div>inside</div> : null}
716
+ {phase < 2 ? null : <div>inside..</div>}
717
+ {phase < 1 ? null : <div>..after</div>}
718
+ {phase < 3 ? <div>after</div> : null}
719
+ {phase < 2 ? null : <div>after..</div>}
720
+ </>
721
+ );
722
+ }
723
+
724
+ const root = ReactDOMClient.createRoot(document.body);
725
+ await act(() => {
726
+ root.render(<App phase={0} />);
727
+ });
728
+ expect(document.documentElement.outerHTML).toBe(
729
+ '<html><head data-h=""><meta itemprop="" content="head"></head><body><div>before</div><div>inside</div><div>after</div></body></html>',
730
+ );
731
+
732
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
733
+ // root of the application
734
+ assertConsoleErrorDev(['In HTML, <head> cannot be a child of <body>']);
735
+
736
+ await act(() => {
737
+ root.render(<App phase={1} />);
738
+ });
739
+ expect(document.documentElement.outerHTML).toBe(
740
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
741
+ );
742
+
743
+ await act(() => {
744
+ root.render(<App phase={2} />);
745
+ });
746
+ expect(document.documentElement.outerHTML).toBe(
747
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
748
+ );
749
+
750
+ await act(() => {
751
+ root.render(<App phase={3} />);
752
+ });
753
+ expect(document.documentElement.outerHTML).toBe(
754
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
755
+ );
756
+
757
+ await act(() => {
758
+ root.unmount();
759
+ });
760
+ expect(document.documentElement.outerHTML).toBe(
761
+ '<html><head></head><body></body></html>',
762
+ );
763
+ });
764
+
765
+ it('should render children of <head> into the document head even when the container is inside the document body', async () => {
766
+ function App({phase}) {
767
+ return (
768
+ <>
769
+ <div>before</div>
770
+ <head data-h="">
771
+ {phase < 1 ? null : <meta itemProp="" content="..head" />}
772
+ {phase < 3 ? <meta itemProp="" content="head" /> : null}
773
+ {phase < 2 ? null : <meta itemProp="" content="head.." />}
774
+ </head>
775
+ <div>after</div>
776
+ </>
777
+ );
778
+ }
779
+
780
+ const container = document.createElement('main');
781
+ document.body.append(container);
782
+ const root = ReactDOMClient.createRoot(container);
783
+ await act(() => {
784
+ root.render(<App phase={0} />);
785
+ });
786
+ expect(document.documentElement.outerHTML).toBe(
787
+ '<html><head data-h=""><meta itemprop="" content="head"></head><body><main><div>before</div><div>after</div></main></body></html>',
788
+ );
789
+
790
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
791
+ // root of the application
792
+ assertConsoleErrorDev(['In HTML, <head> cannot be a child of <main>']);
793
+
794
+ await act(() => {
795
+ root.render(<App phase={1} />);
796
+ });
797
+ expect(document.documentElement.outerHTML).toBe(
798
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body><main><div>before</div><div>after</div></main></body></html>',
799
+ );
800
+
801
+ await act(() => {
802
+ root.render(<App phase={2} />);
803
+ });
804
+ expect(document.documentElement.outerHTML).toBe(
805
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body><main><div>before</div><div>after</div></main></body></html>',
806
+ );
807
+
808
+ await act(() => {
809
+ root.render(<App phase={3} />);
810
+ });
811
+ expect(document.documentElement.outerHTML).toBe(
812
+ '<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body><main><div>before</div><div>after</div></main></body></html>',
813
+ );
814
+
815
+ await act(() => {
816
+ root.unmount();
817
+ });
818
+ expect(document.documentElement.outerHTML).toBe(
819
+ '<html><head></head><body><main></main></body></html>',
820
+ );
821
+ });
822
+
823
+ it('can render a Suspense boundary above the <html> tag', async () => {
824
+ let suspendOnNewPromise;
825
+ let resolveCurrentPromise;
826
+ let currentPromise;
827
+ function createNewPromise() {
828
+ currentPromise = new Promise(r => {
829
+ resolveCurrentPromise = r;
830
+ });
831
+ return currentPromise;
832
+ }
833
+ createNewPromise();
834
+ function Comp() {
835
+ const [promise, setPromise] = React.useState(currentPromise);
836
+ suspendOnNewPromise = () => {
837
+ setPromise(createNewPromise());
838
+ };
839
+ React.use(promise);
840
+ return null;
841
+ }
842
+
843
+ const fallback = (
844
+ <html data-fallback="">
845
+ <body data-fallback="">
846
+ <div>fallback</div>
847
+ </body>
848
+ </html>
849
+ );
850
+
851
+ const main = (
852
+ <html lang="en">
853
+ <head>
854
+ <meta itemProp="" content="primary" />
855
+ </head>
856
+ <body>
857
+ <div>
858
+ <Message />
859
+ </div>
860
+ </body>
861
+ </html>
862
+ );
863
+
864
+ let suspendOnNewMessage;
865
+ let currentMessage;
866
+ let resolveCurrentMessage;
867
+ function createNewMessage() {
868
+ currentMessage = new Promise(r => {
869
+ resolveCurrentMessage = r;
870
+ });
871
+ return currentMessage;
872
+ }
873
+ createNewMessage();
874
+ resolveCurrentMessage('hello world');
875
+ function Message() {
876
+ const [pendingMessage, setPendingMessage] =
877
+ React.useState(currentMessage);
878
+ suspendOnNewMessage = () => {
879
+ setPendingMessage(createNewMessage());
880
+ };
881
+ return React.use(pendingMessage);
882
+ }
883
+
884
+ function App() {
885
+ return (
886
+ <React.Suspense fallback={fallback}>
887
+ <Comp />
888
+ {main}
889
+ </React.Suspense>
890
+ );
891
+ }
892
+
893
+ const root = ReactDOMClient.createRoot(document);
894
+ await act(() => {
895
+ root.render(<App />);
896
+ });
897
+ // The initial render is blocked by promiseA so we see the fallback Document
898
+ expect(document.documentElement.outerHTML).toBe(
899
+ '<html data-fallback=""><head></head><body data-fallback=""><div>fallback</div></body></html>',
900
+ );
901
+
902
+ await act(() => {
903
+ resolveCurrentPromise();
904
+ });
905
+ // When promiseA resolves we see the primary Document
906
+ expect(document.documentElement.outerHTML).toBe(
907
+ '<html lang="en"><head><meta itemprop="" content="primary"></head><body><div>hello world</div></body></html>',
908
+ );
909
+
910
+ await act(() => {
911
+ suspendOnNewPromise();
912
+ });
913
+ // When we switch to rendering ComponentB synchronously we have to put the Document back into fallback
914
+ // The primary content remains hidden until promiseB resolves
915
+ expect(document.documentElement.outerHTML).toBe(
916
+ '<html data-fallback=""><head><meta itemprop="" content="primary" style="display: none;"></head><body data-fallback=""><div style="display: none;">hello world</div><div>fallback</div></body></html>',
917
+ );
918
+
919
+ await act(() => {
920
+ resolveCurrentPromise();
921
+ });
922
+ // When promiseB resolves we see the new primary content inside the primary Document
923
+ // style attributes stick around after being unhidden by the Suspense boundary
924
+ expect(document.documentElement.outerHTML).toBe(
925
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
926
+ );
927
+
928
+ await act(() => {
929
+ React.startTransition(() => {
930
+ suspendOnNewPromise();
931
+ });
932
+ });
933
+ expect(document.documentElement.outerHTML).toBe(
934
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
935
+ );
936
+
937
+ await act(() => {
938
+ resolveCurrentPromise();
939
+ });
940
+ expect(document.documentElement.outerHTML).toBe(
941
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
942
+ );
943
+
944
+ await act(() => {
945
+ suspendOnNewMessage();
946
+ });
947
+ // When we update the message itself we will be causing updates on the primary content of the Suspense boundary.
948
+ // The reason we also test for this is to make sure we don't double acquire the document singletons while
949
+ // disappearing and reappearing layout effects
950
+ expect(document.documentElement.outerHTML).toBe(
951
+ '<html data-fallback=""><head><meta itemprop="" content="primary" style="display: none;"></head><body data-fallback=""><div style="display: none;">hello world</div><div>fallback</div></body></html>',
952
+ );
953
+
954
+ await act(() => {
955
+ resolveCurrentMessage('hello you!');
956
+ });
957
+ expect(document.documentElement.outerHTML).toBe(
958
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello you!</div></body></html>',
959
+ );
960
+
961
+ await act(() => {
962
+ React.startTransition(() => {
963
+ suspendOnNewMessage();
964
+ });
965
+ });
966
+ expect(document.documentElement.outerHTML).toBe(
967
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello you!</div></body></html>',
968
+ );
969
+
970
+ await act(() => {
971
+ resolveCurrentMessage('goodbye!');
972
+ });
973
+ expect(document.documentElement.outerHTML).toBe(
974
+ '<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">goodbye!</div></body></html>',
975
+ );
976
+ });
977
});