1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let React;
    
  13. let ReactDOMClient;
    
  14. let ReactDOMServer;
    
  15. let act;
    
  16. 
    
  17. const util = require('util');
    
  18. const realConsoleError = console.error;
    
  19. 
    
  20. describe('ReactDOMServerHydration', () => {
    
  21.   let container;
    
  22. 
    
  23.   beforeEach(() => {
    
  24.     jest.resetModules();
    
  25.     React = require('react');
    
  26.     ReactDOMClient = require('react-dom/client');
    
  27.     ReactDOMServer = require('react-dom/server');
    
  28.     act = require('react-dom/test-utils').act;
    
  29. 
    
  30.     console.error = jest.fn();
    
  31.     container = document.createElement('div');
    
  32.     document.body.appendChild(container);
    
  33.   });
    
  34. 
    
  35.   afterEach(() => {
    
  36.     document.body.removeChild(container);
    
  37.     console.error = realConsoleError;
    
  38.   });
    
  39. 
    
  40.   function normalizeCodeLocInfo(str) {
    
  41.     return (
    
  42.       typeof str === 'string' &&
    
  43.       str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
    
  44.         return '\n    in ' + name + ' (at **)';
    
  45.       })
    
  46.     );
    
  47.   }
    
  48. 
    
  49.   function formatMessage(args) {
    
  50.     const [format, ...rest] = args;
    
  51.     if (format instanceof Error) {
    
  52.       return 'Caught [' + format.message + ']';
    
  53.     }
    
  54.     if (
    
  55.       format !== null &&
    
  56.       typeof format === 'object' &&
    
  57.       String(format).indexOf('Error: Uncaught [') === 0
    
  58.     ) {
    
  59.       // Ignore errors captured by jsdom and their stacks.
    
  60.       // We only want console errors in this suite.
    
  61.       return null;
    
  62.     }
    
  63.     rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
    
  64.     return util.format(format, ...rest);
    
  65.   }
    
  66. 
    
  67.   function formatConsoleErrors() {
    
  68.     return console.error.mock.calls.map(formatMessage).filter(Boolean);
    
  69.   }
    
  70. 
    
  71.   function testMismatch(Mismatch) {
    
  72.     const htmlString = ReactDOMServer.renderToString(
    
  73.       <Mismatch isClient={false} />,
    
  74.     );
    
  75.     container.innerHTML = htmlString;
    
  76.     act(() => {
    
  77.       ReactDOMClient.hydrateRoot(container, <Mismatch isClient={true} />);
    
  78.     });
    
  79.     return formatConsoleErrors();
    
  80.   }
    
  81. 
    
  82.   describe('text mismatch', () => {
    
  83.     // @gate __DEV__
    
  84.     it('warns when client and server render different text', () => {
    
  85.       function Mismatch({isClient}) {
    
  86.         return (
    
  87.           <div className="parent">
    
  88.             <main className="child">{isClient ? 'client' : 'server'}</main>
    
  89.           </div>
    
  90.         );
    
  91.       }
    
  92.       if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
    
  93.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  94.           [
    
  95.             "Warning: Text content did not match. Server: "server" Client: "client"
    
  96.               in main (at **)
    
  97.               in div (at **)
    
  98.               in Mismatch (at **)",
    
  99.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  100.             "Caught [Text content does not match server-rendered HTML.]",
    
  101.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  102.           ]
    
  103.         `);
    
  104.       } else {
    
  105.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  106.           [
    
  107.             "Warning: Text content did not match. Server: "server" Client: "client"
    
  108.               in main (at **)
    
  109.               in div (at **)
    
  110.               in Mismatch (at **)",
    
  111.           ]
    
  112.         `);
    
  113.       }
    
  114.     });
    
  115. 
    
  116.     // @gate __DEV__
    
  117.     it('warns when client and server render different html', () => {
    
  118.       function Mismatch({isClient}) {
    
  119.         return (
    
  120.           <div className="parent">
    
  121.             <main
    
  122.               className="child"
    
  123.               dangerouslySetInnerHTML={{
    
  124.                 __html: isClient
    
  125.                   ? '<span>client</span>'
    
  126.                   : '<span>server</span>',
    
  127.               }}
    
  128.             />
    
  129.           </div>
    
  130.         );
    
  131.       }
    
  132.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  133.         [
    
  134.           "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: "<span>server</span>" Client: "<span>client</span>"
    
  135.             in main (at **)
    
  136.             in div (at **)
    
  137.             in Mismatch (at **)",
    
  138.         ]
    
  139.       `);
    
  140.     });
    
  141.   });
    
  142. 
    
  143.   describe('attribute mismatch', () => {
    
  144.     // @gate __DEV__
    
  145.     it('warns when client and server render different attributes', () => {
    
  146.       function Mismatch({isClient}) {
    
  147.         return (
    
  148.           <div className="parent">
    
  149.             <main
    
  150.               className={isClient ? 'child client' : 'child server'}
    
  151.               dir={isClient ? 'ltr' : 'rtl'}
    
  152.             />
    
  153.           </div>
    
  154.         );
    
  155.       }
    
  156.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  157.         [
    
  158.           "Warning: Prop \`className\` did not match. Server: "child server" Client: "child client"
    
  159.             in main (at **)
    
  160.             in div (at **)
    
  161.             in Mismatch (at **)",
    
  162.         ]
    
  163.       `);
    
  164.     });
    
  165. 
    
  166.     // @gate __DEV__
    
  167.     it('warns when client renders extra attributes', () => {
    
  168.       function Mismatch({isClient}) {
    
  169.         return (
    
  170.           <div className="parent">
    
  171.             <main
    
  172.               className="child"
    
  173.               tabIndex={isClient ? 1 : null}
    
  174.               dir={isClient ? 'ltr' : null}
    
  175.             />
    
  176.           </div>
    
  177.         );
    
  178.       }
    
  179.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  180.         [
    
  181.           "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
    
  182.             in main (at **)
    
  183.             in div (at **)
    
  184.             in Mismatch (at **)",
    
  185.         ]
    
  186.       `);
    
  187.     });
    
  188. 
    
  189.     // @gate __DEV__
    
  190.     it('warns when server renders extra attributes', () => {
    
  191.       function Mismatch({isClient}) {
    
  192.         return (
    
  193.           <div className="parent">
    
  194.             <main
    
  195.               className="child"
    
  196.               tabIndex={isClient ? null : 1}
    
  197.               dir={isClient ? null : 'rtl'}
    
  198.             />
    
  199.           </div>
    
  200.         );
    
  201.       }
    
  202.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  203.         [
    
  204.           "Warning: Extra attributes from the server: tabindex,dir
    
  205.             in main (at **)
    
  206.             in div (at **)
    
  207.             in Mismatch (at **)",
    
  208.         ]
    
  209.       `);
    
  210.     });
    
  211. 
    
  212.     // @gate __DEV__
    
  213.     it('warns when both client and server render extra attributes', () => {
    
  214.       function Mismatch({isClient}) {
    
  215.         return (
    
  216.           <div className="parent">
    
  217.             <main
    
  218.               className="child"
    
  219.               tabIndex={isClient ? 1 : null}
    
  220.               dir={isClient ? null : 'rtl'}
    
  221.             />
    
  222.           </div>
    
  223.         );
    
  224.       }
    
  225.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  226.         [
    
  227.           "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
    
  228.             in main (at **)
    
  229.             in div (at **)
    
  230.             in Mismatch (at **)",
    
  231.         ]
    
  232.       `);
    
  233.     });
    
  234. 
    
  235.     // @gate __DEV__
    
  236.     it('warns when client and server render different styles', () => {
    
  237.       function Mismatch({isClient}) {
    
  238.         return (
    
  239.           <div className="parent">
    
  240.             <main
    
  241.               className="child"
    
  242.               style={{
    
  243.                 opacity: isClient ? 1 : 0,
    
  244.               }}
    
  245.             />
    
  246.           </div>
    
  247.         );
    
  248.       }
    
  249.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  250.         [
    
  251.           "Warning: Prop \`style\` did not match. Server: "opacity:0" Client: "opacity:1"
    
  252.             in main (at **)
    
  253.             in div (at **)
    
  254.             in Mismatch (at **)",
    
  255.         ]
    
  256.       `);
    
  257.     });
    
  258.   });
    
  259. 
    
  260.   describe('extra nodes on the client', () => {
    
  261.     describe('extra elements on the client', () => {
    
  262.       // @gate __DEV__
    
  263.       it('warns when client renders an extra element as only child', () => {
    
  264.         function Mismatch({isClient}) {
    
  265.           return (
    
  266.             <div className="parent">
    
  267.               {isClient && <main className="only" />}
    
  268.             </div>
    
  269.           );
    
  270.         }
    
  271.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  272.             [
    
  273.               "Warning: Expected server HTML to contain a matching <main> in <div>.
    
  274.                 in main (at **)
    
  275.                 in div (at **)
    
  276.                 in Mismatch (at **)",
    
  277.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  278.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  279.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  280.             ]
    
  281.           `);
    
  282.       });
    
  283. 
    
  284.       // @gate __DEV__
    
  285.       it('warns when client renders an extra element in the beginning', () => {
    
  286.         function Mismatch({isClient}) {
    
  287.           return (
    
  288.             <div className="parent">
    
  289.               {isClient && <header className="1" />}
    
  290.               <main className="2" />
    
  291.               <footer className="3" />
    
  292.             </div>
    
  293.           );
    
  294.         }
    
  295.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  296.             [
    
  297.               "Warning: Expected server HTML to contain a matching <header> in <div>.
    
  298.                 in header (at **)
    
  299.                 in div (at **)
    
  300.                 in Mismatch (at **)",
    
  301.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  302.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  303.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  304.             ]
    
  305.           `);
    
  306.       });
    
  307. 
    
  308.       // @gate __DEV__
    
  309.       it('warns when client renders an extra element in the middle', () => {
    
  310.         function Mismatch({isClient}) {
    
  311.           return (
    
  312.             <div className="parent">
    
  313.               <header className="1" />
    
  314.               {isClient && <main className="2" />}
    
  315.               <footer className="3" />
    
  316.             </div>
    
  317.           );
    
  318.         }
    
  319.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  320.             [
    
  321.               "Warning: Expected server HTML to contain a matching <main> in <div>.
    
  322.                 in main (at **)
    
  323.                 in div (at **)
    
  324.                 in Mismatch (at **)",
    
  325.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  326.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  327.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  328.             ]
    
  329.           `);
    
  330.       });
    
  331. 
    
  332.       // @gate __DEV__
    
  333.       it('warns when client renders an extra element in the end', () => {
    
  334.         function Mismatch({isClient}) {
    
  335.           return (
    
  336.             <div className="parent">
    
  337.               <header className="1" />
    
  338.               <main className="2" />
    
  339.               {isClient && <footer className="3" />}
    
  340.             </div>
    
  341.           );
    
  342.         }
    
  343.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  344.             [
    
  345.               "Warning: Expected server HTML to contain a matching <footer> in <div>.
    
  346.                 in footer (at **)
    
  347.                 in div (at **)
    
  348.                 in Mismatch (at **)",
    
  349.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  350.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  351.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  352.             ]
    
  353.           `);
    
  354.       });
    
  355.     });
    
  356. 
    
  357.     describe('extra text nodes on the client', () => {
    
  358.       // @gate __DEV__
    
  359.       it('warns when client renders an extra text node as only child', () => {
    
  360.         function Mismatch({isClient}) {
    
  361.           return <div className="parent">{isClient && 'only'}</div>;
    
  362.         }
    
  363.         if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
    
  364.           expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  365.             [
    
  366.               "Warning: Text content did not match. Server: "" Client: "only"
    
  367.                 in div (at **)
    
  368.                 in Mismatch (at **)",
    
  369.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  370.               "Caught [Text content does not match server-rendered HTML.]",
    
  371.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  372.             ]
    
  373.           `);
    
  374.         } else {
    
  375.           expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  376.             [
    
  377.               "Warning: Text content did not match. Server: "" Client: "only"
    
  378.                 in div (at **)
    
  379.                 in Mismatch (at **)",
    
  380.             ]
    
  381.           `);
    
  382.         }
    
  383.       });
    
  384. 
    
  385.       // @gate __DEV__
    
  386.       it('warns when client renders an extra text node in the beginning', () => {
    
  387.         function Mismatch({isClient}) {
    
  388.           return (
    
  389.             <div className="parent">
    
  390.               <header className="1" />
    
  391.               {isClient && 'second'}
    
  392.               <footer className="3" />
    
  393.             </div>
    
  394.           );
    
  395.         }
    
  396.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  397.             [
    
  398.               "Warning: Expected server HTML to contain a matching text node for "second" in <div>.
    
  399.                 in div (at **)
    
  400.                 in Mismatch (at **)",
    
  401.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  402.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  403.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  404.             ]
    
  405.           `);
    
  406.       });
    
  407. 
    
  408.       // @gate __DEV__
    
  409.       it('warns when client renders an extra text node in the beginning', () => {
    
  410.         function Mismatch({isClient}) {
    
  411.           return (
    
  412.             <div className="parent">
    
  413.               {isClient && 'first'}
    
  414.               <main className="2" />
    
  415.               <footer className="3" />
    
  416.             </div>
    
  417.           );
    
  418.         }
    
  419.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  420.             [
    
  421.               "Warning: Expected server HTML to contain a matching text node for "first" in <div>.
    
  422.                 in div (at **)
    
  423.                 in Mismatch (at **)",
    
  424.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  425.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  426.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  427.             ]
    
  428.           `);
    
  429.       });
    
  430. 
    
  431.       // @gate __DEV__
    
  432.       it('warns when client renders an extra text node in the end', () => {
    
  433.         function Mismatch({isClient}) {
    
  434.           return (
    
  435.             <div className="parent">
    
  436.               <header className="1" />
    
  437.               <main className="2" />
    
  438.               {isClient && 'third'}
    
  439.             </div>
    
  440.           );
    
  441.         }
    
  442.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  443.             [
    
  444.               "Warning: Expected server HTML to contain a matching text node for "third" in <div>.
    
  445.                 in div (at **)
    
  446.                 in Mismatch (at **)",
    
  447.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  448.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  449.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  450.             ]
    
  451.           `);
    
  452.       });
    
  453.     });
    
  454.   });
    
  455. 
    
  456.   describe('extra nodes on the server', () => {
    
  457.     describe('extra elements on the server', () => {
    
  458.       // @gate __DEV__
    
  459.       it('warns when server renders an extra element as only child', () => {
    
  460.         function Mismatch({isClient}) {
    
  461.           return (
    
  462.             <div className="parent">
    
  463.               {!isClient && <main className="only" />}
    
  464.             </div>
    
  465.           );
    
  466.         }
    
  467.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  468.             [
    
  469.               "Warning: Did not expect server HTML to contain a <main> in <div>.
    
  470.                 in div (at **)
    
  471.                 in Mismatch (at **)",
    
  472.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  473.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  474.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  475.             ]
    
  476.           `);
    
  477.       });
    
  478. 
    
  479.       // @gate __DEV__
    
  480.       it('warns when server renders an extra element in the beginning', () => {
    
  481.         function Mismatch({isClient}) {
    
  482.           return (
    
  483.             <div className="parent">
    
  484.               {!isClient && <header className="1" />}
    
  485.               <main className="2" />
    
  486.               <footer className="3" />
    
  487.             </div>
    
  488.           );
    
  489.         }
    
  490.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  491.           [
    
  492.             "Warning: Expected server HTML to contain a matching <main> in <div>.
    
  493.               in main (at **)
    
  494.               in div (at **)
    
  495.               in Mismatch (at **)",
    
  496.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  497.             "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  498.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  499.           ]
    
  500.         `);
    
  501.       });
    
  502. 
    
  503.       // @gate __DEV__
    
  504.       it('warns when server renders an extra element in the middle', () => {
    
  505.         function Mismatch({isClient}) {
    
  506.           return (
    
  507.             <div className="parent">
    
  508.               <header className="1" />
    
  509.               {!isClient && <main className="2" />}
    
  510.               <footer className="3" />
    
  511.             </div>
    
  512.           );
    
  513.         }
    
  514.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  515.             [
    
  516.               "Warning: Expected server HTML to contain a matching <footer> in <div>.
    
  517.                 in footer (at **)
    
  518.                 in div (at **)
    
  519.                 in Mismatch (at **)",
    
  520.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  521.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  522.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  523.             ]
    
  524.           `);
    
  525.       });
    
  526. 
    
  527.       // @gate __DEV__
    
  528.       it('warns when server renders an extra element in the end', () => {
    
  529.         function Mismatch({isClient}) {
    
  530.           return (
    
  531.             <div className="parent">
    
  532.               <header className="1" />
    
  533.               <main className="2" />
    
  534.               {!isClient && <footer className="3" />}
    
  535.             </div>
    
  536.           );
    
  537.         }
    
  538.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  539.             [
    
  540.               "Warning: Did not expect server HTML to contain a <footer> in <div>.
    
  541.                 in div (at **)
    
  542.                 in Mismatch (at **)",
    
  543.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  544.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  545.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  546.             ]
    
  547.           `);
    
  548.       });
    
  549.     });
    
  550. 
    
  551.     describe('extra text nodes on the server', () => {
    
  552.       // @gate __DEV__
    
  553.       it('warns when server renders an extra text node as only child', () => {
    
  554.         function Mismatch({isClient}) {
    
  555.           return <div className="parent">{!isClient && 'only'}</div>;
    
  556.         }
    
  557.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  558.             [
    
  559.               "Warning: Did not expect server HTML to contain the text node "only" in <div>.
    
  560.                 in div (at **)
    
  561.                 in Mismatch (at **)",
    
  562.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  563.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  564.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  565.             ]
    
  566.           `);
    
  567.       });
    
  568. 
    
  569.       // @gate __DEV__
    
  570.       it('warns when server renders an extra text node in the beginning', () => {
    
  571.         function Mismatch({isClient}) {
    
  572.           return (
    
  573.             <div className="parent">
    
  574.               {!isClient && 'first'}
    
  575.               <main className="2" />
    
  576.               <footer className="3" />
    
  577.             </div>
    
  578.           );
    
  579.         }
    
  580.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  581.           [
    
  582.             "Warning: Expected server HTML to contain a matching <main> in <div>.
    
  583.               in main (at **)
    
  584.               in div (at **)
    
  585.               in Mismatch (at **)",
    
  586.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  587.             "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  588.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  589.           ]
    
  590.         `);
    
  591.       });
    
  592. 
    
  593.       // @gate __DEV__
    
  594.       it('warns when server renders an extra text node in the middle', () => {
    
  595.         function Mismatch({isClient}) {
    
  596.           return (
    
  597.             <div className="parent">
    
  598.               <header className="1" />
    
  599.               {!isClient && 'second'}
    
  600.               <footer className="3" />
    
  601.             </div>
    
  602.           );
    
  603.         }
    
  604.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  605.             [
    
  606.               "Warning: Expected server HTML to contain a matching <footer> in <div>.
    
  607.                 in footer (at **)
    
  608.                 in div (at **)
    
  609.                 in Mismatch (at **)",
    
  610.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  611.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  612.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  613.             ]
    
  614.           `);
    
  615.       });
    
  616. 
    
  617.       // @gate __DEV__
    
  618.       it('warns when server renders an extra text node in the end', () => {
    
  619.         function Mismatch({isClient}) {
    
  620.           return (
    
  621.             <div className="parent">
    
  622.               <header className="1" />
    
  623.               <main className="2" />
    
  624.               {!isClient && 'third'}
    
  625.             </div>
    
  626.           );
    
  627.         }
    
  628.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  629.             [
    
  630.               "Warning: Did not expect server HTML to contain the text node "third" in <div>.
    
  631.                 in div (at **)
    
  632.                 in Mismatch (at **)",
    
  633.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  634.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  635.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  636.             ]
    
  637.           `);
    
  638.       });
    
  639.     });
    
  640.   });
    
  641. 
    
  642.   describe('special nodes', () => {
    
  643.     describe('Suspense', () => {
    
  644.       function Never() {
    
  645.         throw new Promise(resolve => {});
    
  646.       }
    
  647. 
    
  648.       // @gate __DEV__
    
  649.       it('warns when client renders an extra Suspense node in content mode', () => {
    
  650.         function Mismatch({isClient}) {
    
  651.           return (
    
  652.             <div className="parent">
    
  653.               {isClient && (
    
  654.                 <React.Suspense fallback={<p>Loading...</p>}>
    
  655.                   <main className="only" />
    
  656.                 </React.Suspense>
    
  657.               )}
    
  658.             </div>
    
  659.           );
    
  660.         }
    
  661.         // TODO: This message doesn't seem to have any useful details.
    
  662.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  663.             [
    
  664.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  665.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  666.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  667.             ]
    
  668.           `);
    
  669.       });
    
  670. 
    
  671.       // @gate __DEV__
    
  672.       it('warns when server renders an extra Suspense node in content mode', () => {
    
  673.         function Mismatch({isClient}) {
    
  674.           return (
    
  675.             <div className="parent">
    
  676.               {!isClient && (
    
  677.                 <React.Suspense fallback={<p>Loading...</p>}>
    
  678.                   <main className="only" />
    
  679.                 </React.Suspense>
    
  680.               )}
    
  681.             </div>
    
  682.           );
    
  683.         }
    
  684.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  685.             [
    
  686.               "Warning: Did not expect server HTML to contain a <main> in <div>.
    
  687.                 in div (at **)
    
  688.                 in Mismatch (at **)",
    
  689.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  690.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  691.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  692.             ]
    
  693.           `);
    
  694.       });
    
  695. 
    
  696.       // @gate __DEV__
    
  697.       it('warns when client renders an extra Suspense node in fallback mode', () => {
    
  698.         function Mismatch({isClient}) {
    
  699.           return (
    
  700.             <div className="parent">
    
  701.               {isClient && (
    
  702.                 <React.Suspense fallback={<p>Loading...</p>}>
    
  703.                   <main className="only" />
    
  704.                   <Never />
    
  705.                 </React.Suspense>
    
  706.               )}
    
  707.             </div>
    
  708.           );
    
  709.         }
    
  710.         // TODO: This message doesn't seem to have any useful details.
    
  711.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  712.             [
    
  713.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  714.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  715.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  716.             ]
    
  717.           `);
    
  718.       });
    
  719. 
    
  720.       // @gate __DEV__
    
  721.       it('warns when server renders an extra Suspense node in fallback mode', () => {
    
  722.         function Mismatch({isClient}) {
    
  723.           return (
    
  724.             <div className="parent">
    
  725.               {!isClient && (
    
  726.                 <React.Suspense fallback={<p>Loading...</p>}>
    
  727.                   <main className="only" />
    
  728.                   <Never />
    
  729.                 </React.Suspense>
    
  730.               )}
    
  731.             </div>
    
  732.           );
    
  733.         }
    
  734. 
    
  735.         // @TODO changes made to sending Fizz errors to client led to the insertion of templates in client rendered
    
  736.         // suspense boundaries. This leaks in this test becuase the client rendered suspense boundary appears like
    
  737.         // unhydrated tail nodes and this template is the first match. When we add special case handling for client
    
  738.         // rendered suspense boundaries this test will likely change again
    
  739.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  740.             [
    
  741.               "Warning: Did not expect server HTML to contain a <template> in <div>.
    
  742.                 in div (at **)
    
  743.                 in Mismatch (at **)",
    
  744.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  745.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  746.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  747.             ]
    
  748.           `);
    
  749.       });
    
  750. 
    
  751.       // @gate __DEV__
    
  752.       it('warns when client renders an extra node inside Suspense content', () => {
    
  753.         function Mismatch({isClient}) {
    
  754.           return (
    
  755.             <div className="parent">
    
  756.               <React.Suspense fallback={<p>Loading...</p>}>
    
  757.                 <header className="1" />
    
  758.                 {isClient && <main className="second" />}
    
  759.                 <footer className="3" />
    
  760.               </React.Suspense>
    
  761.             </div>
    
  762.           );
    
  763.         }
    
  764.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  765.             [
    
  766.               "Warning: Expected server HTML to contain a matching <main> in <div>.
    
  767.                 in main (at **)
    
  768.                 in Suspense (at **)
    
  769.                 in div (at **)
    
  770.                 in Mismatch (at **)",
    
  771.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  772.               "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
    
  773.             ]
    
  774.           `);
    
  775.       });
    
  776. 
    
  777.       // @gate __DEV__
    
  778.       it('warns when server renders an extra node inside Suspense content', () => {
    
  779.         function Mismatch({isClient}) {
    
  780.           return (
    
  781.             <div className="parent">
    
  782.               <React.Suspense fallback={<p>Loading...</p>}>
    
  783.                 <header className="1" />
    
  784.                 {!isClient && <main className="second" />}
    
  785.                 <footer className="3" />
    
  786.               </React.Suspense>
    
  787.             </div>
    
  788.           );
    
  789.         }
    
  790.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  791.             [
    
  792.               "Warning: Expected server HTML to contain a matching <footer> in <div>.
    
  793.                 in footer (at **)
    
  794.                 in Suspense (at **)
    
  795.                 in div (at **)
    
  796.                 in Mismatch (at **)",
    
  797.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  798.               "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
    
  799.             ]
    
  800.           `);
    
  801.       });
    
  802. 
    
  803.       // @gate __DEV__
    
  804.       it('warns when client renders an extra node inside Suspense fallback', () => {
    
  805.         function Mismatch({isClient}) {
    
  806.           return (
    
  807.             <div className="parent">
    
  808.               <React.Suspense
    
  809.                 fallback={
    
  810.                   <>
    
  811.                     <p>Loading...</p>
    
  812.                     {isClient && <br />}
    
  813.                   </>
    
  814.                 }>
    
  815.                 <main className="only" />
    
  816.                 <Never />
    
  817.               </React.Suspense>
    
  818.             </div>
    
  819.           );
    
  820.         }
    
  821. 
    
  822.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  823.             [
    
  824.               "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
    
  825.             ]
    
  826.           `);
    
  827.       });
    
  828. 
    
  829.       // @gate __DEV__
    
  830.       it('warns when server renders an extra node inside Suspense fallback', () => {
    
  831.         function Mismatch({isClient}) {
    
  832.           return (
    
  833.             <div className="parent">
    
  834.               <React.Suspense
    
  835.                 fallback={
    
  836.                   <>
    
  837.                     <p>Loading...</p>
    
  838.                     {!isClient && <br />}
    
  839.                   </>
    
  840.                 }>
    
  841.                 <main className="only" />
    
  842.                 <Never />
    
  843.               </React.Suspense>
    
  844.             </div>
    
  845.           );
    
  846.         }
    
  847. 
    
  848.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  849.             [
    
  850.               "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
    
  851.             ]
    
  852.           `);
    
  853.       });
    
  854.     });
    
  855. 
    
  856.     describe('Fragment', () => {
    
  857.       // @gate __DEV__
    
  858.       it('warns when client renders an extra Fragment node', () => {
    
  859.         function Mismatch({isClient}) {
    
  860.           return (
    
  861.             <div className="parent">
    
  862.               {isClient && (
    
  863.                 <>
    
  864.                   <header className="1" />
    
  865.                   <main className="2" />
    
  866.                   <footer className="3" />
    
  867.                 </>
    
  868.               )}
    
  869.             </div>
    
  870.           );
    
  871.         }
    
  872.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  873.           [
    
  874.             "Warning: Expected server HTML to contain a matching <header> in <div>.
    
  875.               in header (at **)
    
  876.               in div (at **)
    
  877.               in Mismatch (at **)",
    
  878.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  879.             "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  880.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  881.           ]
    
  882.         `);
    
  883.       });
    
  884. 
    
  885.       // @gate __DEV__
    
  886.       it('warns when server renders an extra Fragment node', () => {
    
  887.         function Mismatch({isClient}) {
    
  888.           return (
    
  889.             <div className="parent">
    
  890.               {!isClient && (
    
  891.                 <>
    
  892.                   <header className="1" />
    
  893.                   <main className="2" />
    
  894.                   <footer className="3" />
    
  895.                 </>
    
  896.               )}
    
  897.             </div>
    
  898.           );
    
  899.         }
    
  900.         expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  901.             [
    
  902.               "Warning: Did not expect server HTML to contain a <header> in <div>.
    
  903.                 in div (at **)
    
  904.                 in Mismatch (at **)",
    
  905.               "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  906.               "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  907.               "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  908.             ]
    
  909.           `);
    
  910.       });
    
  911.     });
    
  912.   });
    
  913. 
    
  914.   describe('misc cases', () => {
    
  915.     // @gate __DEV__
    
  916.     it('warns when client renders an extra node deeper in the tree', () => {
    
  917.       function Mismatch({isClient}) {
    
  918.         return isClient ? <ProfileSettings /> : <MediaSettings />;
    
  919.       }
    
  920. 
    
  921.       function ProfileSettings() {
    
  922.         return (
    
  923.           <div className="parent">
    
  924.             <input />
    
  925.             <Panel type="profile" />
    
  926.           </div>
    
  927.         );
    
  928.       }
    
  929. 
    
  930.       function MediaSettings() {
    
  931.         return (
    
  932.           <div className="parent">
    
  933.             <input />
    
  934.             <Panel type="media" />
    
  935.           </div>
    
  936.         );
    
  937.       }
    
  938. 
    
  939.       function Panel({type}) {
    
  940.         return (
    
  941.           <>
    
  942.             <header className="1" />
    
  943.             <main className="2" />
    
  944.             {type === 'profile' && <footer className="3" />}
    
  945.           </>
    
  946.         );
    
  947.       }
    
  948. 
    
  949.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  950.           [
    
  951.             "Warning: Expected server HTML to contain a matching <footer> in <div>.
    
  952.               in footer (at **)
    
  953.               in Panel (at **)
    
  954.               in div (at **)
    
  955.               in ProfileSettings (at **)
    
  956.               in Mismatch (at **)",
    
  957.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  958.             "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  959.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  960.           ]
    
  961.         `);
    
  962.     });
    
  963. 
    
  964.     // @gate __DEV__
    
  965.     it('warns when server renders an extra node deeper in the tree', () => {
    
  966.       function Mismatch({isClient}) {
    
  967.         return isClient ? <ProfileSettings /> : <MediaSettings />;
    
  968.       }
    
  969. 
    
  970.       function ProfileSettings() {
    
  971.         return (
    
  972.           <div className="parent">
    
  973.             <input />
    
  974.             <Panel type="profile" />
    
  975.           </div>
    
  976.         );
    
  977.       }
    
  978. 
    
  979.       function MediaSettings() {
    
  980.         return (
    
  981.           <div className="parent">
    
  982.             <input />
    
  983.             <Panel type="media" />
    
  984.           </div>
    
  985.         );
    
  986.       }
    
  987. 
    
  988.       function Panel({type}) {
    
  989.         return (
    
  990.           <>
    
  991.             <header className="1" />
    
  992.             <main className="2" />
    
  993.             {type !== 'profile' && <footer className="3" />}
    
  994.           </>
    
  995.         );
    
  996.       }
    
  997. 
    
  998.       expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
    
  999.           [
    
  1000.             "Warning: Did not expect server HTML to contain a <footer> in <div>.
    
  1001.               in div (at **)
    
  1002.               in ProfileSettings (at **)
    
  1003.               in Mismatch (at **)",
    
  1004.             "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
    
  1005.             "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
    
  1006.             "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
    
  1007.           ]
    
  1008.         `);
    
  1009.     });
    
  1010.   });
    
  1011. });