@@ -681,4 +681,181 @@ describe('ReactDOMServerPartialHydration', () => {
681681 let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
682682 expect ( ref . current ) . toBe ( span ) ;
683683 } ) ;
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+ } ) ;
684861} ) ;
0 commit comments