@@ -19,6 +19,15 @@ let SuspenseList;
1919let act ;
2020let IdleEventPriority ;
2121
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+
2231function dispatchMouseEvent ( to , from ) {
2332 if ( ! to ) {
2433 to = null ;
@@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {
240249
241250 // @gate enableClientRenderFallbackOnHydrationMismatch
242251 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+ } ;
243258 let client = false ;
244259 let suspend = false ;
245260 let resolve ;
@@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
276291 </ Suspense >
277292 ) ;
278293 }
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+ ] ) ;
289305
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+ ) ;
293309
294- suspend = true ;
295- client = true ;
310+ suspend = true ;
311+ client = true ;
296312
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 ( ) ;
310326
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+ ) ;
315331
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+ ] ) ;
319353
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+ ) ;
338358
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+ }
343374 } ) ;
344375
345376 it ( 'calls the hydration callbacks after hydration or deletion' , async ( ) => {
@@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
493524 } ) ;
494525
495526 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+ } ;
510533
534+ const ref = React . createRef ( ) ;
511535 let shouldSuspend = false ;
512536 let resolve ;
513537 const promise = new Promise ( res => {
@@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
522546 }
523547 return < > </ > ;
524548 }
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 } /> ) ;
525563
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 ;
530566
531- const span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
567+ const span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
532568
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 ) ;
536572
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+ } ) ;
541577
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 ( ) ;
549583
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 ;
556604 }
557605 } ) ;
558606
@@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
31793227 } ) ;
31803228 } ) ;
31813229 } ) . 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 } ,
31853238 ) ;
31863239 expect ( Scheduler ) . toHaveYielded ( [
31873240 'Log recoverable error: An error occurred during hydration. The server ' +
0 commit comments