1010let JSDOM ;
1111let React ;
1212let ReactDOM ;
13+ let Scheduler ;
1314let clientAct ;
1415let ReactDOMFizzServer ;
1516let Stream ;
17+ let Suspense ;
1618let useId ;
1719let document ;
1820let writable ;
@@ -27,9 +29,11 @@ describe('useId', () => {
2729 JSDOM = require ( 'jsdom' ) . JSDOM ;
2830 React = require ( 'react' ) ;
2931 ReactDOM = require ( 'react-dom' ) ;
32+ Scheduler = require ( 'scheduler' ) ;
3033 clientAct = require ( 'jest-react' ) . act ;
3134 ReactDOMFizzServer = require ( 'react-dom/server' ) ;
3235 Stream = require ( 'stream' ) ;
36+ Suspense = React . Suspense ;
3337 useId = React . unstable_useId ;
3438
3539 // Test Environment
@@ -86,6 +90,11 @@ describe('useId', () => {
8690 }
8791 }
8892
93+ function Text ( { text} ) {
94+ Scheduler . unstable_yieldValue ( text ) ;
95+ return text ;
96+ }
97+
8998 function normalizeTreeIdForTesting ( id ) {
9099 const [ serverClientPrefix , base32 , hookIndex ] = id . split ( ':' ) ;
91100 if ( serverClientPrefix === 'r' ) {
@@ -282,4 +291,126 @@ describe('useId', () => {
282291 expect ( divs [ i ] . id ) . toMatch ( / ^ R : .( ( ( a l ) * a ? ) ( ( l a ) * l ? ) ) * $ / ) ;
283292 }
284293 } ) ;
294+
295+ test ( 'basic incremental hydration' , async ( ) => {
296+ function App ( ) {
297+ return (
298+ < div >
299+ < Suspense fallback = "Loading..." >
300+ < DivWithId label = "A" />
301+ < DivWithId label = "B" />
302+ </ Suspense >
303+ < DivWithId label = "C" />
304+ </ div >
305+ ) ;
306+ }
307+
308+ await serverAct ( async ( ) => {
309+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream ( < App /> ) ;
310+ pipe ( writable ) ;
311+ } ) ;
312+ await clientAct ( async ( ) => {
313+ ReactDOM . hydrateRoot ( container , < App /> ) ;
314+ } ) ;
315+ expect ( container ) . toMatchInlineSnapshot ( `
316+ <div
317+ id="container"
318+ >
319+ <div>
320+ <!--$-->
321+ <div
322+ id="101"
323+ />
324+ <div
325+ id="1001"
326+ />
327+ <!--/$-->
328+ <div
329+ id="10"
330+ />
331+ </div>
332+ </div>
333+ ` ) ;
334+ } ) ;
335+
336+ test ( 'inserting a sibling before a dehydrated Suspense boundary' , async ( ) => {
337+ function App ( { showMore} ) {
338+ const siblings = showMore
339+ ? [ < Text key = "A" text = "A" /> , < Text key = "B" text = "B" /> ]
340+ : [ < Text key = "A" text = "A" /> ] ;
341+
342+ return (
343+ < div >
344+ { siblings }
345+ < Suspense fallback = "Loading..." >
346+ < DivWithId label = "A" />
347+ < DivWithId label = "B" />
348+ </ Suspense >
349+ < DivWithId label = "C" />
350+ </ div >
351+ ) ;
352+ }
353+
354+ await serverAct ( async ( ) => {
355+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream ( < App /> ) ;
356+ pipe ( writable ) ;
357+ } ) ;
358+ expect ( Scheduler ) . toHaveYielded ( [ 'A' ] ) ;
359+ await clientAct ( async ( ) => {
360+ const root = ReactDOM . hydrateRoot ( container , < App /> ) ;
361+ expect ( Scheduler ) . toFlushUntilNextPaint ( [ 'A' ] ) ;
362+ expect ( container ) . toMatchInlineSnapshot ( `
363+ <div
364+ id="container"
365+ >
366+ <div>
367+ A
368+ <!-- -->
369+ <!--$-->
370+ <div
371+ id="110"
372+ />
373+ <div
374+ id="1010"
375+ />
376+ <!--/$-->
377+ <div
378+ id="11"
379+ />
380+ </div>
381+ </div>
382+ ` ) ;
383+ // Insert another sibling before the Suspense boundary
384+ root . render ( < App showMore = { true } /> ) ;
385+ } ) ;
386+ expect ( Scheduler ) . toHaveYielded ( [
387+ 'A' ,
388+ 'B' ,
389+ // The update triggers selective hydration so we render again
390+ 'A' ,
391+ 'B' ,
392+ ] ) ;
393+ expect ( container ) . toMatchInlineSnapshot ( `
394+ <div
395+ id="container"
396+ >
397+ <div>
398+ A
399+ <!-- -->
400+ <!--$-->
401+ B
402+ <div
403+ id="110"
404+ />
405+ <div
406+ id="1010"
407+ />
408+ <!--/$-->
409+ <div
410+ id="11"
411+ />
412+ </div>
413+ </div>
414+ ` ) ;
415+ } ) ;
285416} ) ;
0 commit comments