@@ -634,4 +634,228 @@ describe('ReactDOMServerPartialHydration', () => {
634
634
expect ( ref . current ) . toBe ( span ) ;
635
635
expect ( container . textContent ) . toBe ( 'Hi' ) ;
636
636
} ) ;
637
+
638
+ it ( 'replaces the fallback with client content if it is not rendered by the server' , async ( ) => {
639
+ let suspend = false ;
640
+ let promise = new Promise ( resolvePromise => { } ) ;
641
+ let ref = React . createRef ( ) ;
642
+
643
+ function Child ( ) {
644
+ if ( suspend ) {
645
+ throw promise ;
646
+ } else {
647
+ return 'Hello' ;
648
+ }
649
+ }
650
+
651
+ function App ( ) {
652
+ return (
653
+ < div >
654
+ < Suspense fallback = "Loading..." >
655
+ < span ref = { ref } >
656
+ < Child />
657
+ </ span >
658
+ </ Suspense >
659
+ </ div >
660
+ ) ;
661
+ }
662
+
663
+ // First we render the final HTML. With the streaming renderer
664
+ // this may have suspense points on the server but here we want
665
+ // to test the completed HTML. Don't suspend on the server.
666
+ suspend = true ;
667
+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
668
+ let container = document . createElement ( 'div' ) ;
669
+ container . innerHTML = finalHTML ;
670
+
671
+ expect ( container . getElementsByTagName ( 'span' ) . length ) . toBe ( 0 ) ;
672
+
673
+ // On the client we have the data available quickly for some reason.
674
+ suspend = false ;
675
+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
676
+ root . render ( < App /> ) ;
677
+ jest . runAllTimers ( ) ;
678
+
679
+ expect ( container . textContent ) . toBe ( 'Hello' ) ;
680
+
681
+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
682
+ expect ( ref . current ) . toBe ( span ) ;
683
+ } ) ;
684
+
685
+ it ( 'waits for pending content to come in from the server and then hydrates it' , async ( ) => {
686
+ let suspend = false ;
687
+ let promise = new Promise ( resolvePromise => { } ) ;
688
+ let ref = React . createRef ( ) ;
689
+
690
+ function Child ( ) {
691
+ if ( suspend ) {
692
+ throw promise ;
693
+ } else {
694
+ return 'Hello' ;
695
+ }
696
+ }
697
+
698
+ function App ( ) {
699
+ return (
700
+ < div >
701
+ < Suspense fallback = "Loading..." >
702
+ < span ref = { ref } >
703
+ < Child />
704
+ </ span >
705
+ </ Suspense >
706
+ </ div >
707
+ ) ;
708
+ }
709
+
710
+ // We're going to simulate what Fizz will do during streaming rendering.
711
+
712
+ // First we generate the HTML of the loading state.
713
+ suspend = true ;
714
+ let loadingHTML = ReactDOMServer . renderToString ( < App /> ) ;
715
+ // Then we generate the HTML of the final content.
716
+ suspend = false ;
717
+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
718
+
719
+ let container = document . createElement ( 'div' ) ;
720
+ container . innerHTML = loadingHTML ;
721
+
722
+ let suspenseNode = container . firstChild . firstChild ;
723
+ expect ( suspenseNode . nodeType ) . toBe ( 8 ) ;
724
+ // Put the suspense node in hydration state.
725
+ suspenseNode . data = '$?' ;
726
+
727
+ // This will simulates new content streaming into the document and
728
+ // replacing the fallback with final content.
729
+ function streamInContent ( ) {
730
+ let temp = document . createElement ( 'div' ) ;
731
+ temp . innerHTML = finalHTML ;
732
+ let finalSuspenseNode = temp . firstChild . firstChild ;
733
+ let fallbackContent = suspenseNode . nextSibling ;
734
+ let finalContent = finalSuspenseNode . nextSibling ;
735
+ suspenseNode . parentNode . replaceChild ( finalContent , fallbackContent ) ;
736
+ suspenseNode . data = '$' ;
737
+ if ( suspenseNode . _reactRetry ) {
738
+ suspenseNode . _reactRetry ( ) ;
739
+ }
740
+ }
741
+
742
+ // We're still showing a fallback.
743
+ expect ( container . getElementsByTagName ( 'span' ) . length ) . toBe ( 0 ) ;
744
+
745
+ // Attempt to hydrate the content.
746
+ suspend = false ;
747
+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
748
+ root . render ( < App /> ) ;
749
+ jest . runAllTimers ( ) ;
750
+
751
+ // We're still loading because we're waiting for the server to stream more content.
752
+ expect ( container . textContent ) . toBe ( 'Loading...' ) ;
753
+
754
+ // The server now updates the content in place in the fallback.
755
+ streamInContent ( ) ;
756
+
757
+ // The final HTML is now in place.
758
+ expect ( container . textContent ) . toBe ( 'Hello' ) ;
759
+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
760
+
761
+ // But it is not yet hydrated.
762
+ expect ( ref . current ) . toBe ( null ) ;
763
+
764
+ jest . runAllTimers ( ) ;
765
+
766
+ // Now it's hydrated.
767
+ expect ( ref . current ) . toBe ( span ) ;
768
+ } ) ;
769
+
770
+ it ( 'handles an error on the client if the server ends up erroring' , async ( ) => {
771
+ let suspend = false ;
772
+ let promise = new Promise ( resolvePromise => { } ) ;
773
+ let ref = React . createRef ( ) ;
774
+
775
+ function Child ( ) {
776
+ if ( suspend ) {
777
+ throw promise ;
778
+ } else {
779
+ throw new Error ( 'Error Message' ) ;
780
+ }
781
+ }
782
+
783
+ class ErrorBoundary extends React . Component {
784
+ state = { error : null } ;
785
+ static getDerivedStateFromError ( error ) {
786
+ return { error} ;
787
+ }
788
+ render ( ) {
789
+ if ( this . state . error ) {
790
+ return < div ref = { ref } > { this . state . error . message } </ div > ;
791
+ }
792
+ return this . props . children ;
793
+ }
794
+ }
795
+
796
+ function App ( ) {
797
+ return (
798
+ < ErrorBoundary >
799
+ < div >
800
+ < Suspense fallback = "Loading..." >
801
+ < span ref = { ref } >
802
+ < Child />
803
+ </ span >
804
+ </ Suspense >
805
+ </ div >
806
+ </ ErrorBoundary >
807
+ ) ;
808
+ }
809
+
810
+ // We're going to simulate what Fizz will do during streaming rendering.
811
+
812
+ // First we generate the HTML of the loading state.
813
+ suspend = true ;
814
+ let loadingHTML = ReactDOMServer . renderToString ( < App /> ) ;
815
+
816
+ let container = document . createElement ( 'div' ) ;
817
+ container . innerHTML = loadingHTML ;
818
+
819
+ let suspenseNode = container . firstChild . firstChild ;
820
+ expect ( suspenseNode . nodeType ) . toBe ( 8 ) ;
821
+ // Put the suspense node in hydration state.
822
+ suspenseNode . data = '$?' ;
823
+
824
+ // This will simulates the server erroring and putting the fallback
825
+ // as the final state.
826
+ function streamInError ( ) {
827
+ suspenseNode . data = '$!' ;
828
+ if ( suspenseNode . _reactRetry ) {
829
+ suspenseNode . _reactRetry ( ) ;
830
+ }
831
+ }
832
+
833
+ // We're still showing a fallback.
834
+ expect ( container . getElementsByTagName ( 'span' ) . length ) . toBe ( 0 ) ;
835
+
836
+ // Attempt to hydrate the content.
837
+ suspend = false ;
838
+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
839
+ root . render ( < App /> ) ;
840
+ jest . runAllTimers ( ) ;
841
+
842
+ // We're still loading because we're waiting for the server to stream more content.
843
+ expect ( container . textContent ) . toBe ( 'Loading...' ) ;
844
+
845
+ // The server now updates the content in place in the fallback.
846
+ streamInError ( ) ;
847
+
848
+ // The server errored, but we still haven't hydrated. We don't know if the
849
+ // client will succeed yet, so we still show the loading state.
850
+ expect ( container . textContent ) . toBe ( 'Loading...' ) ;
851
+ expect ( ref . current ) . toBe ( null ) ;
852
+
853
+ jest . runAllTimers ( ) ;
854
+
855
+ // Hydrating should've generated an error and replaced the suspense boundary.
856
+ expect ( container . textContent ) . toBe ( 'Error Message' ) ;
857
+
858
+ let div = container . getElementsByTagName ( 'div' ) [ 0 ] ;
859
+ expect ( ref . current ) . toBe ( div ) ;
860
+ } ) ;
637
861
} ) ;
0 commit comments