@@ -19,6 +19,15 @@ let SuspenseList;
19
19
let act ;
20
20
let IdleEventPriority ;
21
21
22
+ function normalizeCodeLocInfo ( strOrErr ) {
23
+ if ( strOrErr && strOrErr . replace ) {
24
+ return strOrErr . replace ( / \n + (?: a t | i n ) ( [ \S ] + ) [ ^ \n ] * / g, function ( m , name ) {
25
+ return '\n in ' + name + ' (at **)' ;
26
+ } ) ;
27
+ }
28
+ return strOrErr ;
29
+ }
30
+
22
31
function dispatchMouseEvent ( to , from ) {
23
32
if ( ! to ) {
24
33
to = null ;
@@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {
240
249
241
250
// @gate enableClientRenderFallbackOnHydrationMismatch
242
251
it ( 'falls back to client rendering boundary on mismatch' , async ( ) => {
252
+ // We can't use the toErrorDev helper here because this is async.
253
+ const originalConsoleError = console . error ;
254
+ const mockError = jest . fn ( ) ;
255
+ console . error = ( ...args ) => {
256
+ mockError ( ...args . map ( normalizeCodeLocInfo ) ) ;
257
+ } ;
243
258
let client = false ;
244
259
let suspend = false ;
245
260
let resolve ;
@@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
276
291
</ Suspense >
277
292
) ;
278
293
}
279
- const finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
280
- const container = document . createElement ( 'div' ) ;
281
- container . innerHTML = finalHTML ;
282
- expect ( Scheduler ) . toHaveYielded ( [
283
- 'Hello' ,
284
- 'Component' ,
285
- 'Component' ,
286
- 'Component' ,
287
- 'Component' ,
288
- ] ) ;
294
+ try {
295
+ const finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
296
+ const container = document . createElement ( 'div' ) ;
297
+ container . innerHTML = finalHTML ;
298
+ expect ( Scheduler ) . toHaveYielded ( [
299
+ 'Hello' ,
300
+ 'Component' ,
301
+ 'Component' ,
302
+ 'Component' ,
303
+ 'Component' ,
304
+ ] ) ;
289
305
290
- expect ( container . innerHTML ) . toBe (
291
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->' ,
292
- ) ;
306
+ expect ( container . innerHTML ) . toBe (
307
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->' ,
308
+ ) ;
293
309
294
- suspend = true ;
295
- client = true ;
310
+ suspend = true ;
311
+ client = true ;
296
312
297
- ReactDOM . hydrateRoot ( container , < App /> , {
298
- onRecoverableError ( error ) {
299
- Scheduler . unstable_yieldValue ( error . message ) ;
300
- } ,
301
- } ) ;
302
- expect ( Scheduler ) . toFlushAndYield ( [
303
- 'Suspend' ,
304
- 'Component' ,
305
- 'Component' ,
306
- 'Component' ,
307
- 'Component' ,
308
- ] ) ;
309
- jest . runAllTimers ( ) ;
313
+ ReactDOM . hydrateRoot ( container , < App /> , {
314
+ onRecoverableError ( error ) {
315
+ Scheduler . unstable_yieldValue ( error . message ) ;
316
+ } ,
317
+ } ) ;
318
+ expect ( Scheduler ) . toFlushAndYield ( [
319
+ 'Suspend' ,
320
+ 'Component' ,
321
+ 'Component' ,
322
+ 'Component' ,
323
+ 'Component' ,
324
+ ] ) ;
325
+ jest . runAllTimers ( ) ;
310
326
311
- // Unchanged
312
- expect ( container . innerHTML ) . toBe (
313
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->' ,
314
- ) ;
327
+ // Unchanged
328
+ expect ( container . innerHTML ) . toBe (
329
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->' ,
330
+ ) ;
315
331
316
- suspend = false ;
317
- resolve ( ) ;
318
- await promise ;
332
+ suspend = false ;
333
+ resolve ( ) ;
334
+ await promise ;
335
+ expect ( Scheduler ) . toFlushAndYield ( [
336
+ // first pass, mismatches at end
337
+ 'Hello' ,
338
+ 'Component' ,
339
+ 'Component' ,
340
+ 'Component' ,
341
+ 'Component' ,
342
+
343
+ // second pass as client render
344
+ 'Hello' ,
345
+ 'Component' ,
346
+ 'Component' ,
347
+ 'Component' ,
348
+ 'Component' ,
349
+
350
+ // Hydration mismatch is logged
351
+ 'An error occurred during hydration. The server HTML was replaced with client content' ,
352
+ ] ) ;
319
353
320
- expect ( Scheduler ) . toFlushAndYield ( [
321
- // first pass, mismatches at end
322
- 'Hello' ,
323
- 'Component' ,
324
- 'Component' ,
325
- 'Component' ,
326
- 'Component' ,
327
-
328
- // second pass as client render
329
- 'Hello' ,
330
- 'Component' ,
331
- 'Component' ,
332
- 'Component' ,
333
- 'Component' ,
334
-
335
- // Hydration mismatch is logged
336
- 'An error occurred during hydration. The server HTML was replaced with client content' ,
337
- ] ) ;
354
+ // Client rendered - suspense comment nodes removed
355
+ expect ( container . innerHTML ) . toBe (
356
+ 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>' ,
357
+ ) ;
338
358
339
- // Client rendered - suspense comment nodes removed
340
- expect ( container . innerHTML ) . toBe (
341
- 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>' ,
342
- ) ;
359
+ if ( __DEV__ ) {
360
+ expect ( mockError . mock . calls [ 0 ] ) . toEqual ( [
361
+ 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s' ,
362
+ 'div' ,
363
+ 'div' ,
364
+ '\n' +
365
+ ' in div (at **)\n' +
366
+ ' in Component (at **)\n' +
367
+ ' in Suspense (at **)\n' +
368
+ ' in App (at **)' ,
369
+ ] ) ;
370
+ }
371
+ } finally {
372
+ console . error = originalConsoleError ;
373
+ }
343
374
} ) ;
344
375
345
376
it ( 'calls the hydration callbacks after hydration or deletion' , async ( ) => {
@@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
493
524
} ) ;
494
525
495
526
it ( 'recovers with client render when server rendered additional nodes at suspense root after unsuspending' , async ( ) => {
496
- spyOnDev ( console , 'error' ) ;
497
- const ref = React . createRef ( ) ;
498
- function App ( { hasB} ) {
499
- return (
500
- < div >
501
- < Suspense fallback = "Loading..." >
502
- < Suspender />
503
- < span ref = { ref } > A</ span >
504
- { hasB ? < span > B</ span > : null }
505
- </ Suspense >
506
- < div > Sibling</ div >
507
- </ div >
508
- ) ;
509
- }
527
+ // We can't use the toErrorDev helper here because this is async.
528
+ const originalConsoleError = console . error ;
529
+ const mockError = jest . fn ( ) ;
530
+ console . error = ( ...args ) => {
531
+ mockError ( ...args . map ( normalizeCodeLocInfo ) ) ;
532
+ } ;
510
533
534
+ const ref = React . createRef ( ) ;
511
535
let shouldSuspend = false ;
512
536
let resolve ;
513
537
const promise = new Promise ( res => {
@@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
522
546
}
523
547
return < > </ > ;
524
548
}
549
+ function App ( { hasB} ) {
550
+ return (
551
+ < div >
552
+ < Suspense fallback = "Loading..." >
553
+ < Suspender />
554
+ < span ref = { ref } > A</ span >
555
+ { hasB ? < span > B</ span > : null }
556
+ </ Suspense >
557
+ < div > Sibling</ div >
558
+ </ div >
559
+ ) ;
560
+ }
561
+ try {
562
+ const finalHTML = ReactDOMServer . renderToString ( < App hasB = { true } /> ) ;
525
563
526
- const finalHTML = ReactDOMServer . renderToString ( < App hasB = { true } /> ) ;
527
-
528
- const container = document . createElement ( 'div' ) ;
529
- container . innerHTML = finalHTML ;
564
+ const container = document . createElement ( 'div' ) ;
565
+ container . innerHTML = finalHTML ;
530
566
531
- const span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
567
+ const span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
532
568
533
- expect ( container . innerHTML ) . toContain ( '<span>A</span>' ) ;
534
- expect ( container . innerHTML ) . toContain ( '<span>B</span>' ) ;
535
- expect ( ref . current ) . toBe ( null ) ;
569
+ expect ( container . innerHTML ) . toContain ( '<span>A</span>' ) ;
570
+ expect ( container . innerHTML ) . toContain ( '<span>B</span>' ) ;
571
+ expect ( ref . current ) . toBe ( null ) ;
536
572
537
- shouldSuspend = true ;
538
- act ( ( ) => {
539
- ReactDOM . hydrateRoot ( container , < App hasB = { false } /> ) ;
540
- } ) ;
573
+ shouldSuspend = true ;
574
+ act ( ( ) => {
575
+ ReactDOM . hydrateRoot ( container , < App hasB = { false } /> ) ;
576
+ } ) ;
541
577
542
- // await expect(async () => {
543
- resolve ( ) ;
544
- await promise ;
545
- Scheduler . unstable_flushAll ( ) ;
546
- await null ;
547
- jest . runAllTimers ( ) ;
548
- // }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
578
+ resolve ( ) ;
579
+ await promise ;
580
+ Scheduler . unstable_flushAll ( ) ;
581
+ await null ;
582
+ jest . runAllTimers ( ) ;
549
583
550
- expect ( container . innerHTML ) . toContain ( '<span>A</span>' ) ;
551
- expect ( container . innerHTML ) . not . toContain ( '<span>B</span>' ) ;
552
- if ( gate ( flags => flags . enableClientRenderFallbackOnHydrationMismatch ) ) {
553
- expect ( ref . current ) . not . toBe ( span ) ;
554
- } else {
555
- expect ( ref . current ) . toBe ( span ) ;
584
+ expect ( container . innerHTML ) . toContain ( '<span>A</span>' ) ;
585
+ expect ( container . innerHTML ) . not . toContain ( '<span>B</span>' ) ;
586
+ if ( gate ( flags => flags . enableClientRenderFallbackOnHydrationMismatch ) ) {
587
+ expect ( ref . current ) . not . toBe ( span ) ;
588
+ } else {
589
+ expect ( ref . current ) . toBe ( span ) ;
590
+ }
591
+ if ( __DEV__ ) {
592
+ expect ( mockError ) . toHaveBeenCalledWith (
593
+ 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s' ,
594
+ 'span' ,
595
+ 'div' ,
596
+ '\n' +
597
+ ' in Suspense (at **)\n' +
598
+ ' in div (at **)\n' +
599
+ ' in App (at **)' ,
600
+ ) ;
601
+ }
602
+ } finally {
603
+ console . error = originalConsoleError ;
556
604
}
557
605
} ) ;
558
606
@@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
3179
3227
} ) ;
3180
3228
} ) ;
3181
3229
} ) . toErrorDev (
3182
- 'Warning: An error occurred during hydration. ' +
3183
- 'The server HTML was replaced with client content in <div>.' ,
3184
- { withoutStack : true } ,
3230
+ [
3231
+ 'Warning: An error occurred during hydration. ' +
3232
+ 'The server HTML was replaced with client content in <div>.' ,
3233
+ 'Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
3234
+ ' in span (at **)\n' +
3235
+ ' in App (at **)' ,
3236
+ ] ,
3237
+ { withoutStack : 1 } ,
3185
3238
) ;
3186
3239
expect ( Scheduler ) . toHaveYielded ( [
3187
3240
'Log recoverable error: An error occurred during hydration. The server ' +
0 commit comments