@@ -13,6 +13,8 @@ import {createEventTarget} from 'dom-event-testing-library';
1313
1414let React ;
1515let ReactFeatureFlags ;
16+ let ReactDOMServer ;
17+ let Scheduler ;
1618
1719describe ( 'ReactScope' , ( ) => {
1820 beforeEach ( ( ) => {
@@ -21,6 +23,7 @@ describe('ReactScope', () => {
2123 ReactFeatureFlags . enableScopeAPI = true ;
2224 ReactFeatureFlags . enableDeprecatedFlareAPI = true ;
2325 React = require ( 'react' ) ;
26+ Scheduler = require ( 'scheduler' ) ;
2427 } ) ;
2528
2629 if ( ! __EXPERIMENTAL__ ) {
@@ -34,6 +37,7 @@ describe('ReactScope', () => {
3437
3538 beforeEach ( ( ) => {
3639 ReactDOM = require ( 'react-dom' ) ;
40+ ReactDOMServer = require ( 'react-dom/server' ) ;
3741 container = document . createElement ( 'div' ) ;
3842 document . body . appendChild ( container ) ;
3943 } ) ;
@@ -208,7 +212,6 @@ describe('ReactScope', () => {
208212
209213 it ( 'scopes support server-side rendering and hydration' , ( ) => {
210214 const TestScope = React . unstable_createScope ( ) ;
211- const ReactDOMServer = require ( 'react-dom/server' ) ;
212215 const scopeRef = React . createRef ( ) ;
213216 const divRef = React . createRef ( ) ;
214217 const spanRef = React . createRef ( ) ;
@@ -306,6 +309,72 @@ describe('ReactScope', () => {
306309 ReactDOM . render ( null , container ) ;
307310 expect ( scopeRef . current ) . toBe ( null ) ;
308311 } ) ;
312+
313+ it ( 'correctly works with suspended boundaries that are hydrated' , async ( ) => {
314+ let suspend = false ;
315+ let resolve ;
316+ const promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
317+ const ref = React . createRef ( ) ;
318+ const TestScope = React . unstable_createScope ( ) ;
319+ const scopeRef = React . createRef ( ) ;
320+ const testScopeQuery = ( type , props ) => true ;
321+
322+ function Child ( ) {
323+ if ( suspend ) {
324+ throw promise ;
325+ } else {
326+ return 'Hello' ;
327+ }
328+ }
329+
330+ function App ( ) {
331+ return (
332+ < div >
333+ < TestScope ref = { scopeRef } >
334+ < React . Suspense fallback = "Loading..." >
335+ < span ref = { ref } >
336+ < Child />
337+ </ span >
338+ </ React . Suspense >
339+ </ TestScope >
340+ </ div >
341+ ) ;
342+ }
343+
344+ // First we render the final HTML. With the streaming renderer
345+ // this may have suspense points on the server but here we want
346+ // to test the completed HTML. Don't suspend on the server.
347+ suspend = false ;
348+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
349+
350+ let container2 = document . createElement ( 'div' ) ;
351+ container2 . innerHTML = finalHTML ;
352+
353+ let span = container2 . getElementsByTagName ( 'span' ) [ 0 ] ;
354+
355+ // On the client we don't have all data yet but we want to start
356+ // hydrating anyway.
357+ suspend = true ;
358+ let root = ReactDOM . createRoot ( container2 , { hydrate : true } ) ;
359+ root . render ( < App /> ) ;
360+ Scheduler . unstable_flushAll ( ) ;
361+ jest . runAllTimers ( ) ;
362+
363+ // This should not cause a runtime exception, see:
364+ // https://github.com/facebook/react/pull/18184
365+ scopeRef . current . DO_NOT_USE_queryAllNodes ( testScopeQuery ) ;
366+ expect ( ref . current ) . toBe ( null ) ;
367+
368+ // Resolving the promise should continue hydration
369+ suspend = false ;
370+ resolve ( ) ;
371+ await promise ;
372+ Scheduler . unstable_flushAll ( ) ;
373+ jest . runAllTimers ( ) ;
374+
375+ // We should now have hydrated with a ref on the existing span.
376+ expect ( ref . current ) . toBe ( span ) ;
377+ } ) ;
309378 } ) ;
310379
311380 describe ( 'ReactTestRenderer' , ( ) => {
0 commit comments