27
let waitForPaint;
28
let assertLog;
29
30
-function normalizeCodeLocInfo(strOrErr) {
31
- if (strOrErr && strOrErr.replace) {
32
- return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
33
- return '\n in ' + name + ' (at **)';
34
- });
30
+function normalizeError(msg) {
31
+ // Take the first sentence to make it easier to assert on.
32
+ const idx = msg.indexOf('.');
33
+ if (idx > -1) {
34
+ return msg.slice(0, idx + 1);
35
}
36
- return strOrErr;
36
+ return msg;
37
}
38
39
function dispatchMouseEvent(to, from) {
234
suspend = true;
235
ReactDOMClient.hydrateRoot(container, <App />, {
236
onRecoverableError(error) {
237
- Scheduler.log(error.message);
237
+ Scheduler.log(normalizeError(error.message));
238
},
239
});
240
await waitForAll([]);
252
});
253
254
it('falls back to client rendering boundary on mismatch', async () => {
255
- // We can't use the toErrorDev helper here because this is async.
256
- const originalConsoleError = console.error;
257
- const mockError = jest.fn();
258
- console.error = (...args) => {
259
- mockError(...args.map(normalizeCodeLocInfo));
260
- };
255
let client = false;
256
let suspend = false;
257
let resolve;
288
</Suspense>
289
);
290
}
297
- try {
298
- const finalHTML = ReactDOMServer.renderToString(<App />);
299
- const container = document.createElement('section');
300
- container.innerHTML = finalHTML;
301
- assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
302
-
303
- expect(container.innerHTML).toBe(
304
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
305
- );
291
+ const finalHTML = ReactDOMServer.renderToString(<App />);
292
+ const container = document.createElement('section');
293
+ container.innerHTML = finalHTML;
294
+ assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
295
307
- suspend = true;
308
- client = true;
296
+ expect(container.innerHTML).toBe(
297
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
298
+ );
299
310
- ReactDOMClient.hydrateRoot(container, <App />, {
311
- onRecoverableError(error) {
312
- Scheduler.log(error.message);
313
- },
314
- });
315
- await waitForAll(['Suspend']);
316
- jest.runAllTimers();
300
+ suspend = true;
301
+ client = true;
302
318
- // Unchanged
319
- expect(container.innerHTML).toBe(
320
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
321
- );
303
+ ReactDOMClient.hydrateRoot(container, <App />, {
304
+ onRecoverableError(error) {
305
+ Scheduler.log(normalizeError(error.message));
306
+ },
307
+ });
308
+ await waitForAll(['Suspend']);
309
+ jest.runAllTimers();
310
323
- suspend = false;
324
- resolve();
325
- await promise;
326
- await waitForAll([
327
- // first pass, mismatches at end
328
- 'Hello',
329
- 'Component',
330
- 'Component',
331
- 'Component',
332
- 'Component',
333
-
334
- // second pass as client render
335
- 'Hello',
336
- 'Component',
337
- 'Component',
338
- 'Component',
339
- 'Component',
340
-
341
- // Hydration mismatch is logged
342
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
343
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
344
- ]);
311
+ // Unchanged
312
+ expect(container.innerHTML).toBe(
313
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
314
+ );
315
346
- // Client rendered - suspense comment nodes removed
347
- expect(container.innerHTML).toBe(
348
- 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
349
- );
316
+ suspend = false;
317
+ resolve();
318
+ await promise;
319
+ await waitForAll([
320
+ // first pass, mismatches at end
321
+ 'Hello',
322
+ 'Component',
323
+ 'Component',
324
+ 'Component',
325
+ 'Component',
326
+
327
+ // second pass as client render
328
+ 'Hello',
329
+ 'Component',
330
+ 'Component',
331
+ 'Component',
332
+ 'Component',
333
+
334
+ // Hydration mismatch is logged
335
+ "Hydration failed because the server rendered HTML didn't match the client.",
336
+ 'There was an error while hydrating this Suspense boundary.',
337
+ ]);
338
351
- if (__DEV__) {
352
- const lastCall = mockError.mock.calls[mockError.mock.calls.length - 1];
353
- expect(lastCall).toEqual([
354
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
355
- 'article',
356
- 'Suspense',
357
- '\n' +
358
- ' in article (at **)\n' +
359
- ' in Component (at **)\n' +
360
- ' in Suspense (at **)\n' +
361
- ' in App (at **)',
362
- ]);
363
- }
364
- } finally {
365
- console.error = originalConsoleError;
366
- }
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
+ );
343
});
344
345
it('does not show a fallback if mismatch is after suspending', async () => {
370
- // We can't use the toErrorDev helper here because this is async.
371
- const originalConsoleError = console.error;
372
- const mockError = jest.fn();
373
- console.error = (...args) => {
374
- mockError(...args.map(normalizeCodeLocInfo));
375
- };
346
let client = false;
347
let suspend = false;
348
let resolve;
380
</Suspense>
381
);
382
}
413
- try {
414
- const finalHTML = ReactDOMServer.renderToString(<App />);
415
- const container = document.createElement('section');
416
- container.innerHTML = finalHTML;
417
- assertLog(['Hello', 'Component']);
383
+ const finalHTML = ReactDOMServer.renderToString(<App />);
384
+ const container = document.createElement('section');
385
+ container.innerHTML = finalHTML;
386
+ assertLog(['Hello', 'Component']);
387
419
- expect(container.innerHTML).toBe(
420
- '<!--$-->Hello<div>Component</div><!--/$-->',
421
- );
388
+ expect(container.innerHTML).toBe(
389
+ '<!--$-->Hello<div>Component</div><!--/$-->',
390
+ );
391
423
- suspend = true;
424
- client = true;
392
+ suspend = true;
393
+ client = true;
394
426
- ReactDOMClient.hydrateRoot(container, <App />, {
427
- onRecoverableError(error) {
428
- Scheduler.log(error.message);
429
- },
430
- });
431
- await waitForAll(['Suspend']);
432
- jest.runAllTimers();
395
+ ReactDOMClient.hydrateRoot(container, <App />, {
396
+ onRecoverableError(error) {
397
+ Scheduler.log(normalizeError(error.message));
398
+ },
399
+ });
400
+ await waitForAll(['Suspend']);
401
+ jest.runAllTimers();
402
434
- // !! Unchanged, continue showing server content while suspended.
435
- expect(container.innerHTML).toBe(
436
- '<!--$-->Hello<div>Component</div><!--/$-->',
437
- );
403
+ // !! Unchanged, continue showing server content while suspended.
404
+ expect(container.innerHTML).toBe(
405
+ '<!--$-->Hello<div>Component</div><!--/$-->',
406
+ );
407
439
- suspend = false;
440
- resolve();
441
- await promise;
442
- await waitForAll([
443
- // first pass, mismatches at end
444
- 'Hello',
445
- 'Component',
446
- 'Hello',
447
- 'Component',
448
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
449
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
450
- ]);
451
- jest.runAllTimers();
452
-
453
- // Client rendered - suspense comment nodes removed.
454
- expect(container.innerHTML).toBe('Hello<article>Mismatch</article>');
455
- if (__DEV__) {
456
- expect(mockError.mock.calls).toEqual([
457
- [
458
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
459
- 'article',
460
- 'Suspense',
461
- '\n' +
462
- ' in article (at **)\n' +
463
- ' in Component (at **)\n' +
464
- ' in Suspense (at **)\n' +
465
- ' in App (at **)',
466
- ],
467
- ]);
468
- }
469
- } finally {
470
- console.error = originalConsoleError;
471
- }
408
+ suspend = false;
409
+ resolve();
410
+ await promise;
411
+ await waitForAll([
412
+ // first pass, mismatches at end
413
+ 'Hello',
414
+ 'Component',
415
+ 'Hello',
416
+ 'Component',
417
+ "Hydration failed because the server rendered HTML didn't match the client.",
418
+ 'There was an error while hydrating this Suspense boundary.',
419
+ ]);
420
+ jest.runAllTimers();
421
+
422
+ // Client rendered - suspense comment nodes removed.
423
+ expect(container.innerHTML).toBe('Hello<article>Mismatch</article>');
424
});
425
426
it('does not show a fallback if mismatch is child of suspended component', async () => {
475
- // We can't use the toErrorDev helper here because this is async.
476
- const originalConsoleError = console.error;
477
- const mockError = jest.fn();
478
- console.error = (...args) => {
479
- mockError(...args.map(normalizeCodeLocInfo));
480
- };
427
let client = false;
428
let suspend = false;
429
let resolve;
462
</Suspense>
463
);
464
}
519
- try {
520
- const finalHTML = ReactDOMServer.renderToString(<App />);
521
- const container = document.createElement('section');
522
- container.innerHTML = finalHTML;
523
- assertLog(['Hello', 'Component']);
465
+ const finalHTML = ReactDOMServer.renderToString(<App />);
466
+ const container = document.createElement('section');
467
+ container.innerHTML = finalHTML;
468
+ assertLog(['Hello', 'Component']);
469
525
- expect(container.innerHTML).toBe(
526
- '<!--$--><div><div>Component</div></div><!--/$-->',
527
- );
470
+ expect(container.innerHTML).toBe(
471
+ '<!--$--><div><div>Component</div></div><!--/$-->',
472
+ );
473
529
- suspend = true;
530
- client = true;
474
+ suspend = true;
475
+ client = true;
476
532
- ReactDOMClient.hydrateRoot(container, <App />, {
533
- onRecoverableError(error) {
534
- Scheduler.log(error.message);
535
- },
536
- });
537
- await waitForAll(['Suspend']);
538
- jest.runAllTimers();
477
+ ReactDOMClient.hydrateRoot(container, <App />, {
478
+ onRecoverableError(error) {
479
+ Scheduler.log(normalizeError(error.message));
480
+ },
481
+ });
482
+ await waitForAll(['Suspend']);
483
+ jest.runAllTimers();
484
540
- // !! Unchanged, continue showing server content while suspended.
541
- expect(container.innerHTML).toBe(
542
- '<!--$--><div><div>Component</div></div><!--/$-->',
543
- );
485
+ // !! Unchanged, continue showing server content while suspended.
486
+ expect(container.innerHTML).toBe(
487
+ '<!--$--><div><div>Component</div></div><!--/$-->',
488
+ );
489
545
- suspend = false;
546
- resolve();
547
- await promise;
548
- await waitForAll([
549
- // first pass, mismatches at end
550
- 'Hello',
551
- 'Component',
552
- 'Hello',
553
- 'Component',
554
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
555
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
556
- ]);
557
- jest.runAllTimers();
490
+ suspend = false;
491
+ resolve();
492
+ await promise;
493
+ await waitForAll([
494
+ // first pass, mismatches at end
495
+ 'Hello',
496
+ 'Component',
497
+ 'Hello',
498
+ 'Component',
499
+ "Hydration failed because the server rendered HTML didn't match the client.",
500
+ 'There was an error while hydrating this Suspense boundary.',
501
+ ]);
502
+ jest.runAllTimers();
503
559
- // Client rendered - suspense comment nodes removed
560
- expect(container.innerHTML).toBe(
561
- '<div><article>Mismatch</article></div>',
562
- );
563
- if (__DEV__) {
564
- expect(mockError.mock.calls).toEqual([
565
- [
566
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
567
- 'article',
568
- 'div',
569
- '\n' +
570
- ' in article (at **)\n' +
571
- ' in Component (at **)\n' +
572
- ' in div (at **)\n' +
573
- ' in Child (at **)\n' +
574
- ' in Suspense (at **)\n' +
575
- ' in App (at **)',
576
- ],
577
- ]);
578
- }
579
- } finally {
580
- console.error = originalConsoleError;
581
- }
504
+ // Client rendered - suspense comment nodes removed
505
+ expect(container.innerHTML).toBe('<div><article>Mismatch</article></div>');
506
});
507
508
it('does not show a fallback if mismatch is parent and first child suspends', async () => {
585
- // We can't use the toErrorDev helper here because this is async.
586
- const originalConsoleError = console.error;
587
- const mockError = jest.fn();
588
- console.error = (...args) => {
589
- mockError(...args.map(normalizeCodeLocInfo));
590
- };
509
let client = false;
510
let suspend = false;
511
let resolve;
554
</Suspense>
555
);
556
}
639
- try {
640
- const finalHTML = ReactDOMServer.renderToString(<App />);
641
- const container = document.createElement('section');
642
- container.innerHTML = finalHTML;
643
- assertLog(['Component', 'Hello']);
557
+ const finalHTML = ReactDOMServer.renderToString(<App />);
558
+ const container = document.createElement('section');
559
+ container.innerHTML = finalHTML;
560
+ assertLog(['Component', 'Hello']);
561
645
- expect(container.innerHTML).toBe(
646
- '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
647
- );
562
+ expect(container.innerHTML).toBe(
563
+ '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
564
+ );
565
649
- suspend = true;
650
- client = true;
566
+ suspend = true;
567
+ client = true;
568
652
- ReactDOMClient.hydrateRoot(container, <App />, {
653
- onRecoverableError(error) {
654
- Scheduler.log(error.message);
655
- },
656
- });
657
- await waitForAll(['Component', 'Suspend']);
658
- jest.runAllTimers();
569
+ ReactDOMClient.hydrateRoot(container, <App />, {
570
+ onRecoverableError(error) {
571
+ Scheduler.log(normalizeError(error.message));
572
+ },
573
+ });
574
+ await waitForAll(['Component', 'Suspend']);
575
+ jest.runAllTimers();
576
660
- // !! Unchanged, continue showing server content while suspended.
661
- expect(container.innerHTML).toBe(
662
- '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
663
- );
577
+ // !! Unchanged, continue showing server content while suspended.
578
+ expect(container.innerHTML).toBe(
579
+ '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
580
+ );
581
665
- suspend = false;
666
- resolve();
667
- await promise;
668
- await waitForAll([
669
- // first pass, mismatches at end
670
- 'Component',
671
- 'Hello',
672
- 'Component',
673
- 'Hello',
674
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
675
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
676
- ]);
677
- jest.runAllTimers();
582
+ suspend = false;
583
+ resolve();
584
+ await promise;
585
+ await waitForAll([
586
+ // first pass, mismatches at end
587
+ 'Component',
588
+ 'Hello',
589
+ 'Component',
590
+ 'Hello',
591
+ "Hydration failed because the server rendered HTML didn't match the client.",
592
+ 'There was an error while hydrating this Suspense boundary.',
593
+ ]);
594
+ jest.runAllTimers();
595
679
- // Client rendered - suspense comment nodes removed
680
- expect(container.innerHTML).toBe(
681
- '<div><div></div><article>Mismatch</article></div>',
682
- );
683
- if (__DEV__) {
684
- expect(mockError.mock.calls).toEqual([
685
- [
686
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
687
- 'article',
688
- 'div',
689
- '\n' +
690
- ' in article (at **)\n' +
691
- ' in div (at **)\n' +
692
- ' in Component (at **)\n' +
693
- ' in Suspense (at **)\n' +
694
- ' in App (at **)',
695
- ],
696
- ]);
697
- }
698
- } finally {
699
- console.error = originalConsoleError;
700
- }
596
+ // Client rendered - suspense comment nodes removed
597
+ expect(container.innerHTML).toBe(
598
+ '<div><div></div><article>Mismatch</article></div>',
599
+ );
600
});
601
602
it('does show a fallback if mismatch is parent and second child suspends', async () => {
704
- // We can't use the toErrorDev helper here because this is async.
705
- const originalConsoleError = console.error;
706
- const mockError = jest.fn();
707
- console.error = (...args) => {
708
- mockError(...args.map(normalizeCodeLocInfo));
709
- };
603
let client = false;
604
let suspend = false;
605
let resolve;
648
</Suspense>
649
);
650
}
758
- try {
759
- const finalHTML = ReactDOMServer.renderToString(<App />);
760
- const container = document.createElement('section');
761
- container.innerHTML = finalHTML;
762
- assertLog(['Component', 'Hello']);
651
+ const finalHTML = ReactDOMServer.renderToString(<App />);
652
+ const container = document.createElement('section');
653
+ container.innerHTML = finalHTML;
654
+ assertLog(['Component', 'Hello']);
655
764
- expect(container.innerHTML).toBe(
765
- '<!--$--><div><div>Component</div><div></div></div><!--/$-->',
766
- );
656
+ expect(container.innerHTML).toBe(
657
+ '<!--$--><div><div>Component</div><div></div></div><!--/$-->',
658
+ );
659
768
- suspend = true;
769
- client = true;
660
+ suspend = true;
661
+ client = true;
662
771
- ReactDOMClient.hydrateRoot(container, <App />, {
772
- onRecoverableError(error) {
773
- Scheduler.log(error.message);
774
- },
775
- });
776
- await waitForAll([
777
- 'Component',
778
- 'Component',
779
- 'Suspend',
780
- 'Fallback',
781
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
782
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
783
- ]);
784
- jest.runAllTimers();
663
+ ReactDOMClient.hydrateRoot(container, <App />, {
664
+ onRecoverableError(error) {
665
+ Scheduler.log(normalizeError(error.message));
666
+ },
667
+ });
668
+ await waitForAll([
669
+ 'Component',
670
+ 'Component',
671
+ 'Suspend',
672
+ 'Fallback',
673
+ "Hydration failed because the server rendered HTML didn't match the client.",
674
+ 'There was an error while hydrating this Suspense boundary.',
675
+ ]);
676
+ jest.runAllTimers();
677
786
- // !! Client switches to suspense fallback.
787
- expect(container.innerHTML).toBe('Loading...');
678
+ // !! Client switches to suspense fallback.
679
+ expect(container.innerHTML).toBe('Loading...');
680
789
- suspend = false;
790
- resolve();
791
- await promise;
792
- await waitForAll(['Component', 'Hello']);
793
- jest.runAllTimers();
681
+ suspend = false;
682
+ resolve();
683
+ await promise;
684
+ await waitForAll(['Component', 'Hello']);
685
+ jest.runAllTimers();
686
795
- // Client rendered - suspense comment nodes removed
796
- expect(container.innerHTML).toBe(
797
- '<div><article>Mismatch</article><div></div></div>',
798
- );
799
- if (__DEV__) {
800
- expect(mockError.mock.calls).toEqual([
801
- [
802
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
803
- 'article',
804
- 'div',
805
- '\n' +
806
- ' in article (at **)\n' +
807
- ' in div (at **)\n' +
808
- ' in Component (at **)\n' +
809
- ' in Suspense (at **)\n' +
810
- ' in App (at **)',
811
- ],
812
- ]);
813
- }
814
- } finally {
815
- console.error = originalConsoleError;
816
- }
687
+ // Client rendered - suspense comment nodes removed
688
+ expect(container.innerHTML).toBe(
689
+ '<div><article>Mismatch</article><div></div></div>',
690
+ );
691
});
692
693
it('does show a fallback if mismatch is in parent element only', async () => {
820
- // We can't use the toErrorDev helper here because this is async.
821
- const originalConsoleError = console.error;
822
- const mockError = jest.fn();
823
- console.error = (...args) => {
824
- mockError(...args.map(normalizeCodeLocInfo));
825
- };
694
let client = false;
695
let suspend = false;
696
let resolve;
729
</Suspense>
730
);
731
}
864
- try {
865
- const finalHTML = ReactDOMServer.renderToString(<App />);
866
- const container = document.createElement('section');
867
- container.innerHTML = finalHTML;
868
- assertLog(['Component', 'Hello']);
732
+ const finalHTML = ReactDOMServer.renderToString(<App />);
733
+ const container = document.createElement('section');
734
+ container.innerHTML = finalHTML;
735
+ assertLog(['Component', 'Hello']);
736
870
- expect(container.innerHTML).toBe(
871
- '<!--$--><div><div></div></div><!--/$-->',
872
- );
737
+ expect(container.innerHTML).toBe('<!--$--><div><div></div></div><!--/$-->');
738
874
- suspend = true;
875
- client = true;
739
+ suspend = true;
740
+ client = true;
741
877
- ReactDOMClient.hydrateRoot(container, <App />, {
878
- onRecoverableError(error) {
879
- Scheduler.log(error.message);
880
- },
881
- });
882
- await waitForAll([
883
- 'Component',
884
- 'Component',
885
- 'Suspend',
886
- 'Fallback',
887
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
888
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
889
- ]);
890
- jest.runAllTimers();
742
+ ReactDOMClient.hydrateRoot(container, <App />, {
743
+ onRecoverableError(error) {
744
+ Scheduler.log(normalizeError(error.message));
745
+ },
746
+ });
747
+ await waitForAll([
748
+ 'Component',
749
+ 'Component',
750
+ 'Suspend',
751
+ 'Fallback',
752
+ "Hydration failed because the server rendered HTML didn't match the client.",
753
+ 'There was an error while hydrating this Suspense boundary.',
754
+ ]);
755
+ jest.runAllTimers();
756
892
- // !! Client switches to suspense fallback.
893
- expect(container.innerHTML).toBe('Loading...');
757
+ // !! Client switches to suspense fallback.
758
+ expect(container.innerHTML).toBe('Loading...');
759
895
- suspend = false;
896
- resolve();
897
- await promise;
898
- await waitForAll(['Component', 'Hello']);
899
- jest.runAllTimers();
900
-
901
- // Client rendered - suspense comment nodes removed
902
- expect(container.innerHTML).toBe('<article><div></div></article>');
903
- if (__DEV__) {
904
- expect(mockError.mock.calls).toEqual([
905
- [
906
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
907
- 'article',
908
- 'Suspense',
909
- '\n' +
910
- ' in article (at **)\n' +
911
- ' in Component (at **)\n' +
912
- ' in Suspense (at **)\n' +
913
- ' in App (at **)',
914
- ],
915
- ]);
916
- }
917
- } finally {
918
- console.error = originalConsoleError;
919
- }
760
+ suspend = false;
761
+ resolve();
762
+ await promise;
763
+ await waitForAll(['Component', 'Hello']);
764
+ jest.runAllTimers();
765
+
766
+ // Client rendered - suspense comment nodes removed
767
+ expect(container.innerHTML).toBe('<article><div></div></article>');
768
});
769
770
it('does show a fallback if mismatch is before suspending', async () => {
923
- // We can't use the toErrorDev helper here because this is async.
924
- const originalConsoleError = console.error;
925
- const mockError = jest.fn();
926
- console.error = (...args) => {
927
- mockError(...args.map(normalizeCodeLocInfo));
928
- };
771
let client = false;
772
let suspend = false;
773
let resolve;
805
</Suspense>
806
);
807
}
966
- try {
967
- const finalHTML = ReactDOMServer.renderToString(<App />);
968
- const container = document.createElement('section');
969
- container.innerHTML = finalHTML;
970
- assertLog(['Component', 'Hello']);
808
+ const finalHTML = ReactDOMServer.renderToString(<App />);
809
+ const container = document.createElement('section');
810
+ container.innerHTML = finalHTML;
811
+ assertLog(['Component', 'Hello']);
812
972
- expect(container.innerHTML).toBe(
973
- '<!--$--><div>Component</div>Hello<!--/$-->',
974
- );
813
+ expect(container.innerHTML).toBe(
814
+ '<!--$--><div>Component</div>Hello<!--/$-->',
815
+ );
816
976
- suspend = true;
977
- client = true;
817
+ suspend = true;
818
+ client = true;
819
979
- ReactDOMClient.hydrateRoot(container, <App />, {
980
- onRecoverableError(error) {
981
- Scheduler.log(error.message);
982
- },
983
- });
984
- await waitForAll([
985
- 'Component',
986
- 'Component',
987
- 'Suspend',
988
- 'Fallback',
989
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
990
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
991
- ]);
992
- jest.runAllTimers();
820
+ ReactDOMClient.hydrateRoot(container, <App />, {
821
+ onRecoverableError(error) {
822
+ Scheduler.log(normalizeError(error.message));
823
+ },
824
+ });
825
+ await waitForAll([
826
+ 'Component',
827
+ 'Component',
828
+ 'Suspend',
829
+ 'Fallback',
830
+ "Hydration failed because the server rendered HTML didn't match the client.",
831
+ 'There was an error while hydrating this Suspense boundary.',
832
+ ]);
833
+ jest.runAllTimers();
834
994
- // !! Client switches to suspense fallback.
995
- expect(container.innerHTML).toBe('Loading...');
835
+ // !! Client switches to suspense fallback.
836
+ expect(container.innerHTML).toBe('Loading...');
837
997
- suspend = false;
998
- resolve();
999
- await promise;
1000
- await waitForAll([
1001
- // first pass, mismatches at end
1002
- 'Component',
1003
- 'Hello',
1004
- ]);
1005
- jest.runAllTimers();
1006
-
1007
- // Client rendered - suspense comment nodes removed
1008
- expect(container.innerHTML).toBe('<article>Mismatch</article>Hello');
1009
- if (__DEV__) {
1010
- expect(mockError.mock.calls).toEqual([
1011
- [
1012
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1013
- 'article',
1014
- 'Suspense',
1015
- '\n' +
1016
- ' in article (at **)\n' +
1017
- ' in Component (at **)\n' +
1018
- ' in Suspense (at **)\n' +
1019
- ' in App (at **)',
1020
- ],
1021
- ]);
1022
- }
1023
- } finally {
1024
- console.error = originalConsoleError;
1025
- }
838
+ suspend = false;
839
+ resolve();
840
+ await promise;
841
+ await waitForAll([
842
+ // first pass, mismatches at end
843
+ 'Component',
844
+ 'Hello',
845
+ ]);
846
+ jest.runAllTimers();
847
+
848
+ // Client rendered - suspense comment nodes removed
849
+ expect(container.innerHTML).toBe('<article>Mismatch</article>Hello');
850
});
851
852
it('does show a fallback if mismatch is before suspending in a child', async () => {
1029
- // We can't use the toErrorDev helper here because this is async.
1030
- const originalConsoleError = console.error;
1031
- const mockError = jest.fn();
1032
- console.error = (...args) => {
1033
- mockError(...args.map(normalizeCodeLocInfo));
1034
- };
853
let client = false;
854
let suspend = false;
855
let resolve;
889
</Suspense>
890
);
891
}
1074
- try {
1075
- const finalHTML = ReactDOMServer.renderToString(<App />);
1076
- const container = document.createElement('section');
1077
- container.innerHTML = finalHTML;
1078
- assertLog(['Component', 'Hello']);
892
+ const finalHTML = ReactDOMServer.renderToString(<App />);
893
+ const container = document.createElement('section');
894
+ container.innerHTML = finalHTML;
895
+ assertLog(['Component', 'Hello']);
896
1080
- expect(container.innerHTML).toBe(
1081
- '<!--$--><div>Component</div><div>Hello</div><!--/$-->',
1082
- );
897
+ expect(container.innerHTML).toBe(
898
+ '<!--$--><div>Component</div><div>Hello</div><!--/$-->',
899
+ );
900
1084
- suspend = true;
1085
- client = true;
901
+ suspend = true;
902
+ client = true;
903
1087
- ReactDOMClient.hydrateRoot(container, <App />, {
1088
- onRecoverableError(error) {
1089
- Scheduler.log(error.message);
1090
- },
1091
- });
1092
- await waitForAll([
1093
- 'Component',
1094
- 'Component',
1095
- 'Suspend',
1096
- 'Fallback',
1097
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
1098
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1099
- ]);
1100
- jest.runAllTimers();
904
+ ReactDOMClient.hydrateRoot(container, <App />, {
905
+ onRecoverableError(error) {
906
+ Scheduler.log(normalizeError(error.message));
907
+ },
908
+ });
909
+ await waitForAll([
910
+ 'Component',
911
+ 'Component',
912
+ 'Suspend',
913
+ 'Fallback',
914
+ "Hydration failed because the server rendered HTML didn't match the client.",
915
+ 'There was an error while hydrating this Suspense boundary.',
916
+ ]);
917
+ jest.runAllTimers();
918
1102
- // !! Client switches to suspense fallback.
1103
- expect(container.innerHTML).toBe('Loading...');
919
+ // !! Client switches to suspense fallback.
920
+ expect(container.innerHTML).toBe('Loading...');
921
1105
- suspend = false;
1106
- resolve();
1107
- await promise;
1108
- await waitForAll([
1109
- // first pass, mismatches at end
1110
- 'Component',
1111
- 'Hello',
1112
- ]);
1113
- jest.runAllTimers();
922
+ suspend = false;
923
+ resolve();
924
+ await promise;
925
+ await waitForAll([
926
+ // first pass, mismatches at end
927
+ 'Component',
928
+ 'Hello',
929
+ ]);
930
+ jest.runAllTimers();
931
1115
- // Client rendered - suspense comment nodes removed.
1116
- expect(container.innerHTML).toBe(
1117
- '<article>Mismatch</article><div>Hello</div>',
1118
- );
1119
- if (__DEV__) {
1120
- expect(mockError.mock.calls).toEqual([
1121
- [
1122
- 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1123
- 'article',
1124
- 'Suspense',
1125
- '\n' +
1126
- ' in article (at **)\n' +
1127
- ' in Component (at **)\n' +
1128
- ' in Suspense (at **)\n' +
1129
- ' in App (at **)',
1130
- ],
1131
- ]);
1132
- }
1133
- } finally {
1134
- console.error = originalConsoleError;
1135
- }
932
+ // Client rendered - suspense comment nodes removed.
933
+ expect(container.innerHTML).toBe(
934
+ '<article>Mismatch</article><div>Hello</div>',
935
+ );
936
});
937
938
it('calls the hydration callbacks after hydration or deletion', async () => {
995
deleted.push(node);
996
},
997
onRecoverableError(error) {
1198
- Scheduler.log(error.message);
998
+ Scheduler.log(normalizeError(error.message));
999
},
1000
});
1001
await waitForAll([]);
1090
expect(container.innerHTML).toContain('<span>B</span>');
1091
expect(ref.current).toBe(null);
1092
1293
- await expect(async () => {
1294
- await act(() => {
1295
- ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1296
- onRecoverableError(error) {
1297
- Scheduler.log(error.message);
1298
- },
1299
- });
1093
+ await act(() => {
1094
+ ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1095
+ onRecoverableError(error) {
1096
+ Scheduler.log(normalizeError(error.message));
1097
+ },
1098
});
1301
- }).toErrorDev(
1302
- 'Did not expect server HTML to contain a <span> in <Suspense>',
1303
- );
1099
+ });
1100
1101
expect(container.innerHTML).toContain('<span>A</span>');
1102
expect(container.innerHTML).not.toContain('<span>B</span>');
1104
assertLog([
1105
'Server rendered',
1106
'Client rendered',
1311
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
1312
- 'There was an error while hydrating this Suspense boundary. ' +
1313
- 'Switched to client rendering.',
1107
+ "Hydration failed because the server rendered HTML didn't match the client.",
1108
+ 'There was an error while hydrating this Suspense boundary.',
1109
]);
1110
expect(ref.current).not.toBe(span);
1111
});
1112
1113
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
1319
- // We can't use the toErrorDev helper here because this is async.
1320
- const originalConsoleError = console.error;
1321
- const mockError = jest.fn();
1322
- console.error = (...args) => {
1323
- mockError(...args.map(normalizeCodeLocInfo));
1324
- };
1325
-
1114
const ref = React.createRef();
1115
let shouldSuspend = false;
1116
let resolve;
1138
</div>
1139
);
1140
}
1353
- try {
1354
- const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
1141
+ const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
1142
1356
- const container = document.createElement('div');
1357
- container.innerHTML = finalHTML;
1143
+ const container = document.createElement('div');
1144
+ container.innerHTML = finalHTML;
1145
1359
- const span = container.getElementsByTagName('span')[0];
1146
+ const span = container.getElementsByTagName('span')[0];
1147
1361
- expect(container.innerHTML).toContain('<span>A</span>');
1362
- expect(container.innerHTML).toContain('<span>B</span>');
1363
- expect(ref.current).toBe(null);
1148
+ expect(container.innerHTML).toContain('<span>A</span>');
1149
+ expect(container.innerHTML).toContain('<span>B</span>');
1150
+ expect(ref.current).toBe(null);
1151
1365
- shouldSuspend = true;
1366
- await act(() => {
1367
- ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
1368
- });
1152
+ shouldSuspend = true;
1153
+ await act(() => {
1154
+ ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
1155
+ });
1156
1157
+ await expect(async () => {
1158
await act(() => {
1159
resolve();
1160
});
1161
+ }).toErrorDev([
1162
+ "Hydration failed because the server rendered HTML didn't match the client.",
1163
+ 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1164
+ ]);
1165
1374
- expect(container.innerHTML).toContain('<span>A</span>');
1375
- expect(container.innerHTML).not.toContain('<span>B</span>');
1376
- expect(ref.current).not.toBe(span);
1377
- if (__DEV__) {
1378
- expect(mockError).toHaveBeenCalledWith(
1379
- 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
1380
- 'span',
1381
- 'Suspense',
1382
- '\n' +
1383
- ' in Suspense (at **)\n' +
1384
- ' in div (at **)\n' +
1385
- ' in App (at **)',
1386
- );
1387
- }
1388
- } finally {
1389
- console.error = originalConsoleError;
1390
- }
1166
+ expect(container.innerHTML).toContain('<span>A</span>');
1167
+ expect(container.innerHTML).not.toContain('<span>B</span>');
1168
+ expect(ref.current).not.toBe(span);
1169
});
1170
1171
it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
1195
expect(container.innerHTML).toContain('<span>B</span>');
1196
expect(ref.current).toBe(null);
1197
1420
- await expect(async () => {
1421
- await act(() => {
1422
- ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1423
- onRecoverableError(error) {
1424
- Scheduler.log(error.message);
1425
- },
1426
- });
1198
+ await act(() => {
1199
+ ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1200
+ onRecoverableError(error) {
1201
+ Scheduler.log(normalizeError(error.message));
1202
+ },
1203
});
1428
- }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
1204
+ });
1205
assertLog([
1430
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
1431
- 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1206
+ "Hydration failed because the server rendered HTML didn't match the client.",
1207
+ 'There was an error while hydrating this Suspense boundary.',
1208
]);
1209
1210
expect(container.innerHTML).toContain('<span>A</span>');
1563
<App text="Hello" className="hello" />,
1564
{
1565
onRecoverableError(error) {
1790
- Scheduler.log(error.message);
1566
+ Scheduler.log(normalizeError(error.message));
1567
},
1568
},
1569
);
1641
<App text="Hello" className="hello" />,
1642
{
1643
onRecoverableError(error) {
1868
- Scheduler.log(error.message);
1644
+ Scheduler.log(normalizeError(error.message));
1645
},
1646
},
1647
);
1717
<App text="Hello" className="hello" />,
1718
{
1719
onRecoverableError(error) {
1944
- Scheduler.log(error.message);
1720
+ Scheduler.log(normalizeError(error.message));
1721
},
1722
},
1723
);
2025
</Context.Provider>,
2026
{
2027
onRecoverableError(error) {
2252
- Scheduler.log(error.message);
2028
+ Scheduler.log(normalizeError(error.message));
2029
},
2030
},
2031
);
2100
suspend = false;
2101
ReactDOMClient.hydrateRoot(container, <App />, {
2102
onRecoverableError(error) {
2327
- Scheduler.log(error.message);
2103
+ Scheduler.log(normalizeError(error.message));
2104
},
2105
});
2106
if (__DEV__) {
2107
await waitForAll([
2108
'The server did not finish this Suspense boundary: The server used' +
2333
- ' "renderToString" which does not support Suspense. If you intended' +
2334
- ' for this Suspense boundary to render the fallback content on the' +
2335
- ' server consider throwing an Error somewhere within the Suspense boundary.' +
2336
- ' If you intended to have the server wait for the suspended component' +
2337
- ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2109
+ ' "renderToString" which does not support Suspense.',
2110
]);
2111
} else {
2112
await waitForAll([
2113
'The server could not finish this Suspense boundary, likely due to ' +
2342
- 'an error during server rendering. Switched to client rendering.',
2114
+ 'an error during server rendering.',
2115
]);
2116
}
2117
jest.runAllTimers();
2169
suspend = false;
2170
ReactDOMClient.hydrateRoot(container, <App />, {
2171
onRecoverableError(error) {
2400
- Scheduler.log(error.message);
2172
+ Scheduler.log(normalizeError(error.message));
2173
},
2174
});
2175
if (__DEV__) {
2176
await waitForAll([
2177
'The server did not finish this Suspense boundary: The server used' +
2406
- ' "renderToString" which does not support Suspense. If you intended' +
2407
- ' for this Suspense boundary to render the fallback content on the' +
2408
- ' server consider throwing an Error somewhere within the Suspense boundary.' +
2409
- ' If you intended to have the server wait for the suspended component' +
2410
- ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2178
+ ' "renderToString" which does not support Suspense.',
2179
]);
2180
} else {
2181
await waitForAll([
2182
'The server could not finish this Suspense boundary, likely due to ' +
2415
- 'an error during server rendering. Switched to client rendering.',
2183
+ 'an error during server rendering.',
2184
]);
2185
}
2186
// This will have exceeded the suspended time so we should timeout.
2243
suspend = false;
2244
ReactDOMClient.hydrateRoot(container, <App />, {
2245
onRecoverableError(error) {
2478
- Scheduler.log(error.message);
2246
+ Scheduler.log(normalizeError(error.message));
2247
},
2248
});
2249
if (__DEV__) {
2250
await waitForAll([
2251
'The server did not finish this Suspense boundary: The server used' +
2484
- ' "renderToString" which does not support Suspense. If you intended' +
2485
- ' for this Suspense boundary to render the fallback content on the' +
2486
- ' server consider throwing an Error somewhere within the Suspense boundary.' +
2487
- ' If you intended to have the server wait for the suspended component' +
2488
- ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2252
+ ' "renderToString" which does not support Suspense.',
2253
]);
2254
} else {
2255
await waitForAll([
2256
'The server could not finish this Suspense boundary, likely due to ' +
2493
- 'an error during server rendering. Switched to client rendering.',
2257
+ 'an error during server rendering.',
2258
]);
2259
}
2260
// This will have exceeded the suspended time so we should timeout.
2561
2562
ReactDOMClient.hydrateRoot(container, <App />, {
2563
onRecoverableError(error) {
2800
- Scheduler.log(error.message);
2564
+ Scheduler.log(normalizeError(error.message));
2565
},
2566
});
2567
2569
if (__DEV__) {
2570
await waitForAll([
2571
'The server did not finish this Suspense boundary: The server used' +
2808
- ' "renderToString" which does not support Suspense. If you intended' +
2809
- ' for this Suspense boundary to render the fallback content on the' +
2810
- ' server consider throwing an Error somewhere within the Suspense boundary.' +
2811
- ' If you intended to have the server wait for the suspended component' +
2812
- ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2572
+ ' "renderToString" which does not support Suspense.',
2573
]);
2574
} else {
2575
await waitForAll([
2576
'The server could not finish this Suspense boundary, likely due to ' +
2817
- 'an error during server rendering. Switched to client rendering.',
2577
+ 'an error during server rendering.',
2578
]);
2579
}
2580
2633
suspend = false;
2634
ReactDOMClient.hydrateRoot(container, <App />, {
2635
onRecoverableError(error) {
2876
- Scheduler.log(error.message);
2636
+ Scheduler.log(normalizeError(error.message));
2637
},
2638
});
2639
if (__DEV__) {
2640
await waitForAll([
2641
'The server did not finish this Suspense boundary: The server used' +
2882
- ' "renderToString" which does not support Suspense. If you intended' +
2883
- ' for this Suspense boundary to render the fallback content on the' +
2884
- ' server consider throwing an Error somewhere within the Suspense boundary.' +
2885
- ' If you intended to have the server wait for the suspended component' +
2886
- ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2642
+ ' "renderToString" which does not support Suspense.',
2643
]);
2644
} else {
2645
await waitForAll([
2646
'The server could not finish this Suspense boundary, likely due to ' +
2891
- 'an error during server rendering. Switched to client rendering.',
2647
+ 'an error during server rendering.',
2648
]);
2649
}
2650
jest.runAllTimers();
2745
</ClassName.Provider>,
2746
{
2747
onRecoverableError(error) {
2992
- Scheduler.log(error.message);
2748
+ Scheduler.log(normalizeError(error.message));
2749
},
2750
},
2751
);
3783
await act(() => {
3784
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3785
onRecoverableError(error) {
4030
- Scheduler.log('Log recoverable error: ' + error.message);
3786
+ Scheduler.log(
3787
+ 'Log recoverable error: ' + normalizeError(error.message),
3788
+ );
3789
},
3790
});
3791
});
3793
[
3794
'Warning: An error occurred during hydration. ' +
3795
'The server HTML was replaced with client content.',
4038
- 'Warning: Expected server HTML to contain a matching <span> in the root.\n' +
4039
- ' in span (at **)\n' +
4040
- ' in App (at **)',
3796
],
3797
{withoutStack: 1},
3798
);
3799
assertLog([
4045
- 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
4046
- 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
3800
+ "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
3801
+ 'Log recoverable error: There was an error while hydrating.',
3802
]);
3803
3804
// We show fallback state when mismatch happens at root
3828
await act(() => {
3829
ReactDOMClient.hydrateRoot(container, <DirectTextChild text="bad" />, {
3830
onRecoverableError(error) {
4076
- Scheduler.log(error.message);
3831
+ Scheduler.log(normalizeError(error.message));
3832
},
3833
});
3834
});
3835
}).toErrorDev(
3836
[
4082
- 'Text content did not match. Server: "good" Client: "bad"',
3837
'An error occurred during hydration. The server HTML was replaced with ' +
3838
'client content.',
3839
],
3840
{withoutStack: 1},
3841
);
3842
assertLog([
4089
- 'Text content does not match server-rendered HTML.',
4090
- 'There was an error while hydrating. Because the error happened outside ' +
4091
- 'of a Suspense boundary, the entire root will switch to client rendering.',
3843
+ "Hydration failed because the server rendered HTML didn't match the client.",
3844
+ 'There was an error while hydrating.',
3845
]);
3846
});
3847
3869
<TextChildWithSibling text="bad" />,
3870
{
3871
onRecoverableError(error) {
4119
- Scheduler.log(error.message);
3872
+ Scheduler.log(normalizeError(error.message));
3873
},
3874
},
3875
);
3876
});
3877
}).toErrorDev(
3878
[
4126
- 'Text content did not match. Server: "good" Client: "bad"',
3879
'An error occurred during hydration. The server HTML was replaced with ' +
3880
'client content.',
3881
],
3882
{withoutStack: 1},
3883
);
3884
assertLog([
4133
- 'Text content does not match server-rendered HTML.',
4134
- 'There was an error while hydrating. Because the error happened outside ' +
4135
- 'of a Suspense boundary, the entire root will switch to client rendering.',
3885
+ "Hydration failed because the server rendered HTML didn't match the client.",
3886
+ 'There was an error while hydrating.',
3887
]);
3888
});
3889
});