@samitouri / QOS-React-2 / commits / d3037405c4

Test showing mismatches after suspending force fallbacks to be shown (#28299)

While investigating https://github.com/facebook/react/issues/28285 I found a possible bug in handling Suspense and mismatches. As the tests show, if the first sibling in a boundary suspends, and the second has a mismatch, we will NOT show a fallback. If the first sibling is a mismatch, and the second sibling suspends, we WILL show a fallback. [Here's a stackbliz showing the behavior on Canary](https://stackblitz.com/edit/stackblitz-starters-bh3snf?file=src%2Fstyle.css,public%2Findex.html,src%2Findex.tsx). This breakage was introduced by: https://github.com/facebook/react/pull/26380. Before this PR, we would not show a fallback in either case. That PR makes it so that we don't pre-render siblings of suspended trees, so presumably, whatever detection we had to avoid fallbacks on mismatches, requires knowing there's a mismatch in the tree when we suspend.

Ricky committed Mar 25, 2024 at 20:33 UTC d3037405c4c9ce8158ef9017e960ec97fb089df4
1 file changed +769
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+769
@@ -366,6 +366,775 @@ describe('ReactDOMServerPartialHydration', () => {
366 }
367 });
368
369 + 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 + };
376 + let client = false;
377 + let suspend = false;
378 + let resolve;
379 + const promise = new Promise(resolvePromise => {
380 + resolve = () => {
381 + suspend = false;
382 + resolvePromise();
383 + };
384 + });
385 + function Child() {
386 + if (suspend) {
387 + Scheduler.log('Suspend');
388 + throw promise;
389 + } else {
390 + Scheduler.log('Hello');
391 + return 'Hello';
392 + }
393 + }
394 + function Component({shouldMismatch}) {
395 + Scheduler.log('Component');
396 + if (shouldMismatch && client) {
397 + return <article>Mismatch</article>;
398 + }
399 + return <div>Component</div>;
400 + }
401 + function Fallback() {
402 + Scheduler.log('Fallback');
403 + return 'Loading...';
404 + }
405 + function App() {
406 + return (
407 + <Suspense fallback={<Fallback />}>
408 + <Child />
409 + <Component shouldMismatch={true} />
410 + </Suspense>
411 + );
412 + }
413 + try {
414 + const finalHTML = ReactDOMServer.renderToString(<App />);
415 + const container = document.createElement('section');
416 + container.innerHTML = finalHTML;
417 + assertLog(['Hello', 'Component']);
418 +
419 + expect(container.innerHTML).toBe(
420 + '<!--$-->Hello<div>Component</div><!--/$-->',
421 + );
422 +
423 + suspend = true;
424 + client = true;
425 +
426 + ReactDOMClient.hydrateRoot(container, <App />, {
427 + onRecoverableError(error) {
428 + Scheduler.log(error.message);
429 + },
430 + });
431 + await waitForAll(['Suspend']);
432 + jest.runAllTimers();
433 +
434 + // !! Unchanged, continue showing server content while suspended.
435 + expect(container.innerHTML).toBe(
436 + '<!--$-->Hello<div>Component</div><!--/$-->',
437 + );
438 +
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 + const secondToLastCall =
457 + mockError.mock.calls[mockError.mock.calls.length - 2];
458 + expect(secondToLastCall).toEqual([
459 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
460 + 'article',
461 + 'section',
462 + '\n' +
463 + ' in article (at **)\n' +
464 + ' in Component (at **)\n' +
465 + ' in Suspense (at **)\n' +
466 + ' in App (at **)',
467 + ]);
468 + }
469 + } finally {
470 + console.error = originalConsoleError;
471 + }
472 + });
473 +
474 + 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 + };
481 + let client = false;
482 + let suspend = false;
483 + let resolve;
484 + const promise = new Promise(resolvePromise => {
485 + resolve = () => {
486 + suspend = false;
487 + resolvePromise();
488 + };
489 + });
490 + function Child({children}) {
491 + if (suspend) {
492 + Scheduler.log('Suspend');
493 + throw promise;
494 + } else {
495 + Scheduler.log('Hello');
496 + return <div>{children}</div>;
497 + }
498 + }
499 + function Component({shouldMismatch}) {
500 + Scheduler.log('Component');
501 + if (shouldMismatch && client) {
502 + return <article>Mismatch</article>;
503 + }
504 + return <div>Component</div>;
505 + }
506 + function Fallback() {
507 + Scheduler.log('Fallback');
508 + return 'Loading...';
509 + }
510 + function App() {
511 + return (
512 + <Suspense fallback={<Fallback />}>
513 + <Child>
514 + <Component shouldMismatch={true} />
515 + </Child>
516 + </Suspense>
517 + );
518 + }
519 + try {
520 + const finalHTML = ReactDOMServer.renderToString(<App />);
521 + const container = document.createElement('section');
522 + container.innerHTML = finalHTML;
523 + assertLog(['Hello', 'Component']);
524 +
525 + expect(container.innerHTML).toBe(
526 + '<!--$--><div><div>Component</div></div><!--/$-->',
527 + );
528 +
529 + suspend = true;
530 + client = true;
531 +
532 + ReactDOMClient.hydrateRoot(container, <App />, {
533 + onRecoverableError(error) {
534 + Scheduler.log(error.message);
535 + },
536 + });
537 + await waitForAll(['Suspend']);
538 + jest.runAllTimers();
539 +
540 + // !! Unchanged, continue showing server content while suspended.
541 + expect(container.innerHTML).toBe(
542 + '<!--$--><div><div>Component</div></div><!--/$-->',
543 + );
544 +
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();
558 +
559 + // Client rendered - suspense comment nodes removed
560 + expect(container.innerHTML).toBe(
561 + '<div><article>Mismatch</article></div>',
562 + );
563 + if (__DEV__) {
564 + const secondToLastCall =
565 + mockError.mock.calls[mockError.mock.calls.length - 2];
566 + expect(secondToLastCall).toEqual([
567 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
568 + 'article',
569 + 'div',
570 + '\n' +
571 + ' in article (at **)\n' +
572 + ' in Component (at **)\n' +
573 + ' in div (at **)\n' +
574 + ' in Child (at **)\n' +
575 + ' in Suspense (at **)\n' +
576 + ' in App (at **)',
577 + ]);
578 + }
579 + } finally {
580 + console.error = originalConsoleError;
581 + }
582 + });
583 +
584 + 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 + };
591 + let client = false;
592 + let suspend = false;
593 + let resolve;
594 + const promise = new Promise(resolvePromise => {
595 + resolve = () => {
596 + suspend = false;
597 + resolvePromise();
598 + };
599 + });
600 + function Child({children}) {
601 + if (suspend) {
602 + Scheduler.log('Suspend');
603 + throw promise;
604 + } else {
605 + Scheduler.log('Hello');
606 + return <div>{children}</div>;
607 + }
608 + }
609 + function Component({shouldMismatch, children}) {
610 + Scheduler.log('Component');
611 + if (shouldMismatch && client) {
612 + return (
613 + <div>
614 + {children}
615 + <article>Mismatch</article>
616 + </div>
617 + );
618 + }
619 + return (
620 + <div>
621 + {children}
622 + <div>Component</div>
623 + </div>
624 + );
625 + }
626 + function Fallback() {
627 + Scheduler.log('Fallback');
628 + return 'Loading...';
629 + }
630 + function App() {
631 + return (
632 + <Suspense fallback={<Fallback />}>
633 + <Component shouldMismatch={true}>
634 + <Child />
635 + </Component>
636 + </Suspense>
637 + );
638 + }
639 + try {
640 + const finalHTML = ReactDOMServer.renderToString(<App />);
641 + const container = document.createElement('section');
642 + container.innerHTML = finalHTML;
643 + assertLog(['Component', 'Hello']);
644 +
645 + expect(container.innerHTML).toBe(
646 + '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
647 + );
648 +
649 + suspend = true;
650 + client = true;
651 +
652 + ReactDOMClient.hydrateRoot(container, <App />, {
653 + onRecoverableError(error) {
654 + Scheduler.log(error.message);
655 + },
656 + });
657 + await waitForAll(['Component', 'Suspend']);
658 + jest.runAllTimers();
659 +
660 + // !! Unchanged, continue showing server content while suspended.
661 + expect(container.innerHTML).toBe(
662 + '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
663 + );
664 +
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();
678 +
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 + const secondToLastCall =
685 + mockError.mock.calls[mockError.mock.calls.length - 2];
686 + expect(secondToLastCall).toEqual([
687 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
688 + 'article',
689 + 'div',
690 + '\n' +
691 + ' in article (at **)\n' +
692 + ' in div (at **)\n' +
693 + ' in Component (at **)\n' +
694 + ' in Suspense (at **)\n' +
695 + ' in App (at **)',
696 + ]);
697 + }
698 + } finally {
699 + console.error = originalConsoleError;
700 + }
701 + });
702 +
703 + 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 + };
710 + let client = false;
711 + let suspend = false;
712 + let resolve;
713 + const promise = new Promise(resolvePromise => {
714 + resolve = () => {
715 + suspend = false;
716 + resolvePromise();
717 + };
718 + });
719 + function Child({children}) {
720 + if (suspend) {
721 + Scheduler.log('Suspend');
722 + throw promise;
723 + } else {
724 + Scheduler.log('Hello');
725 + return <div>{children}</div>;
726 + }
727 + }
728 + function Component({shouldMismatch, children}) {
729 + Scheduler.log('Component');
730 + if (shouldMismatch && client) {
731 + return (
732 + <div>
733 + <article>Mismatch</article>
734 + {children}
735 + </div>
736 + );
737 + }
738 + return (
739 + <div>
740 + <div>Component</div>
741 + {children}
742 + </div>
743 + );
744 + }
745 + function Fallback() {
746 + Scheduler.log('Fallback');
747 + return 'Loading...';
748 + }
749 + function App() {
750 + return (
751 + <Suspense fallback={<Fallback />}>
752 + <Component shouldMismatch={true}>
753 + <Child />
754 + </Component>
755 + </Suspense>
756 + );
757 + }
758 + try {
759 + const finalHTML = ReactDOMServer.renderToString(<App />);
760 + const container = document.createElement('section');
761 + container.innerHTML = finalHTML;
762 + assertLog(['Component', 'Hello']);
763 +
764 + expect(container.innerHTML).toBe(
765 + '<!--$--><div><div>Component</div><div></div></div><!--/$-->',
766 + );
767 +
768 + suspend = true;
769 + client = true;
770 +
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();
785 +
786 + // !! Client switches to suspense fallback.
787 + expect(container.innerHTML).toBe('Loading...');
788 +
789 + suspend = false;
790 + resolve();
791 + await promise;
792 + await waitForAll(['Component', 'Hello']);
793 + jest.runAllTimers();
794 +
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 + const secondToLastCall =
801 + mockError.mock.calls[mockError.mock.calls.length - 2];
802 + expect(secondToLastCall).toEqual([
803 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
804 + 'article',
805 + 'div',
806 + '\n' +
807 + ' in article (at **)\n' +
808 + ' in div (at **)\n' +
809 + ' in Component (at **)\n' +
810 + ' in Suspense (at **)\n' +
811 + ' in App (at **)',
812 + ]);
813 + }
814 + } finally {
815 + console.error = originalConsoleError;
816 + }
817 + });
818 +
819 + 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 + };
826 + let client = false;
827 + let suspend = false;
828 + let resolve;
829 + const promise = new Promise(resolvePromise => {
830 + resolve = () => {
831 + suspend = false;
832 + resolvePromise();
833 + };
834 + });
835 + function Child({children}) {
836 + if (suspend) {
837 + Scheduler.log('Suspend');
838 + throw promise;
839 + } else {
840 + Scheduler.log('Hello');
841 + return <div>{children}</div>;
842 + }
843 + }
844 + function Component({shouldMismatch, children}) {
845 + Scheduler.log('Component');
846 + if (shouldMismatch && client) {
847 + return <article>{children}</article>;
848 + }
849 + return <div>{children}</div>;
850 + }
851 + function Fallback() {
852 + Scheduler.log('Fallback');
853 + return 'Loading...';
854 + }
855 + function App() {
856 + return (
857 + <Suspense fallback={<Fallback />}>
858 + <Component shouldMismatch={true}>
859 + <Child />
860 + </Component>
861 + </Suspense>
862 + );
863 + }
864 + try {
865 + const finalHTML = ReactDOMServer.renderToString(<App />);
866 + const container = document.createElement('section');
867 + container.innerHTML = finalHTML;
868 + assertLog(['Component', 'Hello']);
869 +
870 + expect(container.innerHTML).toBe(
871 + '<!--$--><div><div></div></div><!--/$-->',
872 + );
873 +
874 + suspend = true;
875 + client = true;
876 +
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();
891 +
892 + // !! Client switches to suspense fallback.
893 + expect(container.innerHTML).toBe('Loading...');
894 +
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 + const secondToLastCall =
905 + mockError.mock.calls[mockError.mock.calls.length - 2];
906 + expect(secondToLastCall).toEqual([
907 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
908 + 'article',
909 + 'section',
910 + '\n' +
911 + ' in article (at **)\n' +
912 + ' in Component (at **)\n' +
913 + ' in Suspense (at **)\n' +
914 + ' in App (at **)',
915 + ]);
916 + }
917 + } finally {
918 + console.error = originalConsoleError;
919 + }
920 + });
921 +
922 + 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 + };
929 + let client = false;
930 + let suspend = false;
931 + let resolve;
932 + const promise = new Promise(resolvePromise => {
933 + resolve = () => {
934 + suspend = false;
935 + resolvePromise();
936 + };
937 + });
938 + function Child() {
939 + if (suspend) {
940 + Scheduler.log('Suspend');
941 + throw promise;
942 + } else {
943 + Scheduler.log('Hello');
944 + return 'Hello';
945 + }
946 + }
947 + function Component({shouldMismatch}) {
948 + Scheduler.log('Component');
949 + if (shouldMismatch && client) {
950 + return <article>Mismatch</article>;
951 + }
952 + return <div>Component</div>;
953 + }
954 + function Fallback() {
955 + Scheduler.log('Fallback');
956 + return 'Loading...';
957 + }
958 + function App() {
959 + return (
960 + <Suspense fallback={<Fallback />}>
961 + <Component shouldMismatch={true} />
962 + <Child />
963 + </Suspense>
964 + );
965 + }
966 + try {
967 + const finalHTML = ReactDOMServer.renderToString(<App />);
968 + const container = document.createElement('section');
969 + container.innerHTML = finalHTML;
970 + assertLog(['Component', 'Hello']);
971 +
972 + expect(container.innerHTML).toBe(
973 + '<!--$--><div>Component</div>Hello<!--/$-->',
974 + );
975 +
976 + suspend = true;
977 + client = true;
978 +
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();
993 +
994 + // !! Client switches to suspense fallback.
995 + expect(container.innerHTML).toBe('Loading...');
996 +
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 + const secondToLastCall =
1011 + mockError.mock.calls[mockError.mock.calls.length - 2];
1012 + expect(secondToLastCall).toEqual([
1013 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1014 + 'article',
1015 + 'section',
1016 + '\n' +
1017 + ' in article (at **)\n' +
1018 + ' in Component (at **)\n' +
1019 + ' in Suspense (at **)\n' +
1020 + ' in App (at **)',
1021 + ]);
1022 + }
1023 + } finally {
1024 + console.error = originalConsoleError;
1025 + }
1026 + });
1027 +
1028 + 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 + };
1035 + let client = false;
1036 + let suspend = false;
1037 + let resolve;
1038 + const promise = new Promise(resolvePromise => {
1039 + resolve = () => {
1040 + suspend = false;
1041 + resolvePromise();
1042 + };
1043 + });
1044 + function Child() {
1045 + if (suspend) {
1046 + Scheduler.log('Suspend');
1047 + throw promise;
1048 + } else {
1049 + Scheduler.log('Hello');
1050 + return 'Hello';
1051 + }
1052 + }
1053 + function Component({shouldMismatch}) {
1054 + Scheduler.log('Component');
1055 + if (shouldMismatch && client) {
1056 + return <article>Mismatch</article>;
1057 + }
1058 + return <div>Component</div>;
1059 + }
1060 + function Fallback() {
1061 + Scheduler.log('Fallback');
1062 + return 'Loading...';
1063 + }
1064 + function App() {
1065 + return (
1066 + <Suspense fallback={<Fallback />}>
1067 + <Component shouldMismatch={true} />
1068 + <div>
1069 + <Child />
1070 + </div>
1071 + </Suspense>
1072 + );
1073 + }
1074 + try {
1075 + const finalHTML = ReactDOMServer.renderToString(<App />);
1076 + const container = document.createElement('section');
1077 + container.innerHTML = finalHTML;
1078 + assertLog(['Component', 'Hello']);
1079 +
1080 + expect(container.innerHTML).toBe(
1081 + '<!--$--><div>Component</div><div>Hello</div><!--/$-->',
1082 + );
1083 +
1084 + suspend = true;
1085 + client = true;
1086 +
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();
1101 +
1102 + // !! Client switches to suspense fallback.
1103 + expect(container.innerHTML).toBe('Loading...');
1104 +
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();
1114 +
1115 + // Client rendered - suspense comment nodes removed.
1116 + expect(container.innerHTML).toBe(
1117 + '<article>Mismatch</article><div>Hello</div>',
1118 + );
1119 + if (__DEV__) {
1120 + const secondToLastCall =
1121 + mockError.mock.calls[mockError.mock.calls.length - 2];
1122 + expect(secondToLastCall).toEqual([
1123 + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1124 + 'article',
1125 + 'section',
1126 + '\n' +
1127 + ' in article (at **)\n' +
1128 + ' in Component (at **)\n' +
1129 + ' in Suspense (at **)\n' +
1130 + ' in App (at **)',
1131 + ]);
1132 + }
1133 + } finally {
1134 + console.error = originalConsoleError;
1135 + }
1136 + });
1137 +
1138 it('calls the hydration callbacks after hydration or deletion', async () => {
1139 let suspend = false;
1140 let resolve;