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.  * @emails react-core
    
  8.  * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
    
  9.  */
    
  10. 
    
  11. let JSDOM;
    
  12. let React;
    
  13. let ReactDOMClient;
    
  14. let clientAct;
    
  15. let ReactDOMFizzServer;
    
  16. let Stream;
    
  17. let Suspense;
    
  18. let useId;
    
  19. let useState;
    
  20. let document;
    
  21. let writable;
    
  22. let container;
    
  23. let buffer = '';
    
  24. let hasErrored = false;
    
  25. let fatalError = undefined;
    
  26. let waitForPaint;
    
  27. 
    
  28. describe('useId', () => {
    
  29.   beforeEach(() => {
    
  30.     jest.resetModules();
    
  31.     JSDOM = require('jsdom').JSDOM;
    
  32.     React = require('react');
    
  33.     ReactDOMClient = require('react-dom/client');
    
  34.     clientAct = require('internal-test-utils').act;
    
  35.     ReactDOMFizzServer = require('react-dom/server');
    
  36.     Stream = require('stream');
    
  37.     Suspense = React.Suspense;
    
  38.     useId = React.useId;
    
  39.     useState = React.useState;
    
  40. 
    
  41.     const InternalTestUtils = require('internal-test-utils');
    
  42.     waitForPaint = InternalTestUtils.waitForPaint;
    
  43. 
    
  44.     // Test Environment
    
  45.     const jsdom = new JSDOM(
    
  46.       '<!DOCTYPE html><html><head></head><body><div id="container">',
    
  47.       {
    
  48.         runScripts: 'dangerously',
    
  49.       },
    
  50.     );
    
  51.     document = jsdom.window.document;
    
  52.     container = document.getElementById('container');
    
  53. 
    
  54.     buffer = '';
    
  55.     hasErrored = false;
    
  56. 
    
  57.     writable = new Stream.PassThrough();
    
  58.     writable.setEncoding('utf8');
    
  59.     writable.on('data', chunk => {
    
  60.       buffer += chunk;
    
  61.     });
    
  62.     writable.on('error', error => {
    
  63.       hasErrored = true;
    
  64.       fatalError = error;
    
  65.     });
    
  66.   });
    
  67. 
    
  68.   async function serverAct(callback) {
    
  69.     await callback();
    
  70.     // Await one turn around the event loop.
    
  71.     // This assumes that we'll flush everything we have so far.
    
  72.     await new Promise(resolve => {
    
  73.       setImmediate(resolve);
    
  74.     });
    
  75.     if (hasErrored) {
    
  76.       throw fatalError;
    
  77.     }
    
  78.     // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
    
  79.     // We also want to execute any scripts that are embedded.
    
  80.     // We assume that we have now received a proper fragment of HTML.
    
  81.     const bufferedContent = buffer;
    
  82.     buffer = '';
    
  83.     const fakeBody = document.createElement('body');
    
  84.     fakeBody.innerHTML = bufferedContent;
    
  85.     while (fakeBody.firstChild) {
    
  86.       const node = fakeBody.firstChild;
    
  87.       if (node.nodeName === 'SCRIPT') {
    
  88.         const script = document.createElement('script');
    
  89.         script.textContent = node.textContent;
    
  90.         fakeBody.removeChild(node);
    
  91.         container.appendChild(script);
    
  92.       } else {
    
  93.         container.appendChild(node);
    
  94.       }
    
  95.     }
    
  96.   }
    
  97. 
    
  98.   function normalizeTreeIdForTesting(id) {
    
  99.     const result = id.match(/:(R|r)([a-z0-9]*)(H([0-9]*))?:/);
    
  100.     if (result === undefined) {
    
  101.       throw new Error('Invalid id format');
    
  102.     }
    
  103.     const [, serverClientPrefix, base32, hookIndex] = result;
    
  104.     if (serverClientPrefix.endsWith('r')) {
    
  105.       // Client ids aren't stable. For testing purposes, strip out the counter.
    
  106.       return (
    
  107.         'CLIENT_GENERATED_ID' +
    
  108.         (hookIndex !== undefined ? ` (${hookIndex})` : '')
    
  109.       );
    
  110.     }
    
  111.     // Formats the tree id as a binary sequence, so it's easier to visualize
    
  112.     // the structure.
    
  113.     return (
    
  114.       parseInt(base32, 32).toString(2) +
    
  115.       (hookIndex !== undefined ? ` (${hookIndex})` : '')
    
  116.     );
    
  117.   }
    
  118. 
    
  119.   function DivWithId({children}) {
    
  120.     const id = normalizeTreeIdForTesting(useId());
    
  121.     return <div id={id}>{children}</div>;
    
  122.   }
    
  123. 
    
  124.   test('basic example', async () => {
    
  125.     function App() {
    
  126.       return (
    
  127.         <div>
    
  128.           <div>
    
  129.             <DivWithId />
    
  130.             <DivWithId />
    
  131.           </div>
    
  132.           <DivWithId />
    
  133.         </div>
    
  134.       );
    
  135.     }
    
  136. 
    
  137.     await serverAct(async () => {
    
  138.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  139.       pipe(writable);
    
  140.     });
    
  141.     await clientAct(async () => {
    
  142.       ReactDOMClient.hydrateRoot(container, <App />);
    
  143.     });
    
  144.     expect(container).toMatchInlineSnapshot(`
    
  145.       <div
    
  146.         id="container"
    
  147.       >
    
  148.         <div>
    
  149.           <div>
    
  150.             <div
    
  151.               id="101"
    
  152.             />
    
  153.             <div
    
  154.               id="1001"
    
  155.             />
    
  156.           </div>
    
  157.           <div
    
  158.             id="10"
    
  159.           />
    
  160.         </div>
    
  161.       </div>
    
  162.     `);
    
  163.   });
    
  164. 
    
  165.   test('indirections', async () => {
    
  166.     function App() {
    
  167.       // There are no forks in this tree, but the parent and the child should
    
  168.       // have different ids.
    
  169.       return (
    
  170.         <DivWithId>
    
  171.           <div>
    
  172.             <div>
    
  173.               <div>
    
  174.                 <DivWithId />
    
  175.               </div>
    
  176.             </div>
    
  177.           </div>
    
  178.         </DivWithId>
    
  179.       );
    
  180.     }
    
  181. 
    
  182.     await serverAct(async () => {
    
  183.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  184.       pipe(writable);
    
  185.     });
    
  186.     await clientAct(async () => {
    
  187.       ReactDOMClient.hydrateRoot(container, <App />);
    
  188.     });
    
  189.     expect(container).toMatchInlineSnapshot(`
    
  190.       <div
    
  191.         id="container"
    
  192.       >
    
  193.         <div
    
  194.           id="0"
    
  195.         >
    
  196.           <div>
    
  197.             <div>
    
  198.               <div>
    
  199.                 <div
    
  200.                   id="1"
    
  201.                 />
    
  202.               </div>
    
  203.             </div>
    
  204.           </div>
    
  205.         </div>
    
  206.       </div>
    
  207.     `);
    
  208.   });
    
  209. 
    
  210.   test('StrictMode double rendering', async () => {
    
  211.     const {StrictMode} = React;
    
  212. 
    
  213.     function App() {
    
  214.       return (
    
  215.         <StrictMode>
    
  216.           <DivWithId />
    
  217.         </StrictMode>
    
  218.       );
    
  219.     }
    
  220. 
    
  221.     await serverAct(async () => {
    
  222.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  223.       pipe(writable);
    
  224.     });
    
  225.     await clientAct(async () => {
    
  226.       ReactDOMClient.hydrateRoot(container, <App />);
    
  227.     });
    
  228.     expect(container).toMatchInlineSnapshot(`
    
  229.       <div
    
  230.         id="container"
    
  231.       >
    
  232.         <div
    
  233.           id="0"
    
  234.         />
    
  235.       </div>
    
  236.     `);
    
  237.   });
    
  238. 
    
  239.   test('empty (null) children', async () => {
    
  240.     // We don't treat empty children different from non-empty ones, which means
    
  241.     // they get allocated a slot when generating ids. There's no inherent reason
    
  242.     // to do this; Fiber happens to allocate a fiber for null children that
    
  243.     // appear in a list, which is not ideal for performance. For the purposes
    
  244.     // of id generation, though, what matters is that Fizz and Fiber
    
  245.     // are consistent.
    
  246.     function App() {
    
  247.       return (
    
  248.         <>
    
  249.           {null}
    
  250.           <DivWithId />
    
  251.           {null}
    
  252.           <DivWithId />
    
  253.         </>
    
  254.       );
    
  255.     }
    
  256. 
    
  257.     await serverAct(async () => {
    
  258.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  259.       pipe(writable);
    
  260.     });
    
  261.     await clientAct(async () => {
    
  262.       ReactDOMClient.hydrateRoot(container, <App />);
    
  263.     });
    
  264.     expect(container).toMatchInlineSnapshot(`
    
  265.       <div
    
  266.         id="container"
    
  267.       >
    
  268.         <div
    
  269.           id="10"
    
  270.         />
    
  271.         <div
    
  272.           id="100"
    
  273.         />
    
  274.       </div>
    
  275.     `);
    
  276.   });
    
  277. 
    
  278.   test('large ids', async () => {
    
  279.     // The component in this test outputs a recursive tree of nodes with ids,
    
  280.     // where the underlying binary representation is an alternating series of 1s
    
  281.     // and 0s. In other words, they are all of the form 101010101.
    
  282.     //
    
  283.     // Because we use base 32 encoding, the resulting id should consist of
    
  284.     // alternating 'a' (01010) and 'l' (10101) characters, except for the the
    
  285.     // 'R:' prefix, and the first character after that, which may not correspond
    
  286.     // to a complete set of 5 bits.
    
  287.     //
    
  288.     // Example: :Rclalalalalalalala...:
    
  289.     //
    
  290.     // We can use this pattern to test large ids that exceed the bitwise
    
  291.     // safe range (32 bits). The algorithm should theoretically support ids
    
  292.     // of any size.
    
  293. 
    
  294.     function Child({children}) {
    
  295.       const id = useId();
    
  296.       return <div id={id}>{children}</div>;
    
  297.     }
    
  298. 
    
  299.     function App() {
    
  300.       let tree = <Child />;
    
  301.       for (let i = 0; i < 50; i++) {
    
  302.         tree = (
    
  303.           <>
    
  304.             <Child />
    
  305.             {tree}
    
  306.           </>
    
  307.         );
    
  308.       }
    
  309.       return tree;
    
  310.     }
    
  311. 
    
  312.     await serverAct(async () => {
    
  313.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  314.       pipe(writable);
    
  315.     });
    
  316.     await clientAct(async () => {
    
  317.       ReactDOMClient.hydrateRoot(container, <App />);
    
  318.     });
    
  319.     const divs = container.querySelectorAll('div');
    
  320. 
    
  321.     // Confirm that every id matches the expected pattern
    
  322.     for (let i = 0; i < divs.length; i++) {
    
  323.       // Example: :Rclalalalalalalala...:
    
  324.       expect(divs[i].id).toMatch(/^:R.(((al)*a?)((la)*l?))*:$/);
    
  325.     }
    
  326.   });
    
  327. 
    
  328.   test('multiple ids in a single component', async () => {
    
  329.     function App() {
    
  330.       const id1 = useId();
    
  331.       const id2 = useId();
    
  332.       const id3 = useId();
    
  333.       return `${id1}, ${id2}, ${id3}`;
    
  334.     }
    
  335. 
    
  336.     await serverAct(async () => {
    
  337.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  338.       pipe(writable);
    
  339.     });
    
  340.     await clientAct(async () => {
    
  341.       ReactDOMClient.hydrateRoot(container, <App />);
    
  342.     });
    
  343.     // We append a suffix to the end of the id to distinguish them
    
  344.     expect(container).toMatchInlineSnapshot(`
    
  345.       <div
    
  346.         id="container"
    
  347.       >
    
  348.         :R0:, :R0H1:, :R0H2:
    
  349.       </div>
    
  350.     `);
    
  351.   });
    
  352. 
    
  353.   test('local render phase updates', async () => {
    
  354.     function App({swap}) {
    
  355.       const [count, setCount] = useState(0);
    
  356.       if (count < 3) {
    
  357.         setCount(count + 1);
    
  358.       }
    
  359.       return useId();
    
  360.     }
    
  361. 
    
  362.     await serverAct(async () => {
    
  363.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  364.       pipe(writable);
    
  365.     });
    
  366.     await clientAct(async () => {
    
  367.       ReactDOMClient.hydrateRoot(container, <App />);
    
  368.     });
    
  369.     expect(container).toMatchInlineSnapshot(`
    
  370.       <div
    
  371.         id="container"
    
  372.       >
    
  373.         :R0:
    
  374.       </div>
    
  375.     `);
    
  376.   });
    
  377. 
    
  378.   test('basic incremental hydration', async () => {
    
  379.     function App() {
    
  380.       return (
    
  381.         <div>
    
  382.           <Suspense fallback="Loading...">
    
  383.             <DivWithId label="A" />
    
  384.             <DivWithId label="B" />
    
  385.           </Suspense>
    
  386.           <DivWithId label="C" />
    
  387.         </div>
    
  388.       );
    
  389.     }
    
  390. 
    
  391.     await serverAct(async () => {
    
  392.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  393.       pipe(writable);
    
  394.     });
    
  395.     await clientAct(async () => {
    
  396.       ReactDOMClient.hydrateRoot(container, <App />);
    
  397.     });
    
  398.     expect(container).toMatchInlineSnapshot(`
    
  399.       <div
    
  400.         id="container"
    
  401.       >
    
  402.         <div>
    
  403.           <!--$-->
    
  404.           <div
    
  405.             id="101"
    
  406.           />
    
  407.           <div
    
  408.             id="1001"
    
  409.           />
    
  410.           <!--/$-->
    
  411.           <div
    
  412.             id="10"
    
  413.           />
    
  414.         </div>
    
  415.       </div>
    
  416.     `);
    
  417.   });
    
  418. 
    
  419.   test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
    
  420.     const span = React.createRef(null);
    
  421.     function App({swap}) {
    
  422.       // Note: Using a dynamic array so these are treated as insertions and
    
  423.       // deletions instead of updates, because Fiber currently allocates a node
    
  424.       // even for empty children.
    
  425.       const children = [
    
  426.         <DivWithId key="A" />,
    
  427.         swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
    
  428.         <DivWithId key="D" />,
    
  429.       ];
    
  430.       return (
    
  431.         <>
    
  432.           {children}
    
  433.           <Suspense key="boundary" fallback="Loading...">
    
  434.             <DivWithId />
    
  435.             <span ref={span} />
    
  436.           </Suspense>
    
  437.         </>
    
  438.       );
    
  439.     }
    
  440. 
    
  441.     await serverAct(async () => {
    
  442.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  443.       pipe(writable);
    
  444.     });
    
  445.     const dehydratedSpan = container.getElementsByTagName('span')[0];
    
  446.     await clientAct(async () => {
    
  447.       const root = ReactDOMClient.hydrateRoot(container, <App />);
    
  448.       await waitForPaint([]);
    
  449.       expect(container).toMatchInlineSnapshot(`
    
  450.         <div
    
  451.           id="container"
    
  452.         >
    
  453.           <div
    
  454.             id="101"
    
  455.           />
    
  456.           <div
    
  457.             id="1001"
    
  458.           />
    
  459.           <div
    
  460.             id="1101"
    
  461.           />
    
  462.           <!--$-->
    
  463.           <div
    
  464.             id="110"
    
  465.           />
    
  466.           <span />
    
  467.           <!--/$-->
    
  468.         </div>
    
  469.       `);
    
  470. 
    
  471.       // The inner boundary hasn't hydrated yet
    
  472.       expect(span.current).toBe(null);
    
  473. 
    
  474.       // Swap B for C
    
  475.       root.render(<App swap={true} />);
    
  476.     });
    
  477.     // The swap should not have caused a mismatch.
    
  478.     expect(container).toMatchInlineSnapshot(`
    
  479.       <div
    
  480.         id="container"
    
  481.       >
    
  482.         <div
    
  483.           id="101"
    
  484.         />
    
  485.         <div
    
  486.           id="CLIENT_GENERATED_ID"
    
  487.         />
    
  488.         <div
    
  489.           id="1101"
    
  490.         />
    
  491.         <!--$-->
    
  492.         <div
    
  493.           id="110"
    
  494.         />
    
  495.         <span />
    
  496.         <!--/$-->
    
  497.       </div>
    
  498.     `);
    
  499.     // Should have hydrated successfully
    
  500.     expect(span.current).toBe(dehydratedSpan);
    
  501.   });
    
  502. 
    
  503.   test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => {
    
  504.     const span = React.createRef(null);
    
  505.     function App({swap}) {
    
  506.       // Note: Using a dynamic array so these are treated as insertions and
    
  507.       // deletions instead of updates, because Fiber currently allocates a node
    
  508.       // even for empty children.
    
  509.       const children = [
    
  510.         <DivWithId key="A" />,
    
  511.         swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
    
  512.         <DivWithId key="D" />,
    
  513.       ];
    
  514.       return (
    
  515.         <Suspense key="boundary" fallback="Loading...">
    
  516.           {children}
    
  517.           <span ref={span} />
    
  518.         </Suspense>
    
  519.       );
    
  520.     }
    
  521. 
    
  522.     await serverAct(async () => {
    
  523.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  524.       pipe(writable);
    
  525.     });
    
  526.     const dehydratedSpan = container.getElementsByTagName('span')[0];
    
  527.     await clientAct(async () => {
    
  528.       const root = ReactDOMClient.hydrateRoot(container, <App />);
    
  529.       await waitForPaint([]);
    
  530.       expect(container).toMatchInlineSnapshot(`
    
  531.         <div
    
  532.           id="container"
    
  533.         >
    
  534.           <!--$-->
    
  535.           <div
    
  536.             id="101"
    
  537.           />
    
  538.           <div
    
  539.             id="1001"
    
  540.           />
    
  541.           <div
    
  542.             id="1101"
    
  543.           />
    
  544.           <span />
    
  545.           <!--/$-->
    
  546.         </div>
    
  547.       `);
    
  548. 
    
  549.       // The inner boundary hasn't hydrated yet
    
  550.       expect(span.current).toBe(null);
    
  551. 
    
  552.       // Swap B for C
    
  553.       root.render(<App swap={true} />);
    
  554.     });
    
  555.     // The swap should not have caused a mismatch.
    
  556.     expect(container).toMatchInlineSnapshot(`
    
  557.       <div
    
  558.         id="container"
    
  559.       >
    
  560.         <!--$-->
    
  561.         <div
    
  562.           id="101"
    
  563.         />
    
  564.         <div
    
  565.           id="CLIENT_GENERATED_ID"
    
  566.         />
    
  567.         <div
    
  568.           id="1101"
    
  569.         />
    
  570.         <span />
    
  571.         <!--/$-->
    
  572.       </div>
    
  573.     `);
    
  574.     // Should have hydrated successfully
    
  575.     expect(span.current).toBe(dehydratedSpan);
    
  576.   });
    
  577. 
    
  578.   test('identifierPrefix option', async () => {
    
  579.     function Child() {
    
  580.       const id = useId();
    
  581.       return <div>{id}</div>;
    
  582.     }
    
  583. 
    
  584.     function App({showMore}) {
    
  585.       return (
    
  586.         <>
    
  587.           <Child />
    
  588.           <Child />
    
  589.           {showMore && <Child />}
    
  590.         </>
    
  591.       );
    
  592.     }
    
  593. 
    
  594.     await serverAct(async () => {
    
  595.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
    
  596.         identifierPrefix: 'custom-prefix-',
    
  597.       });
    
  598.       pipe(writable);
    
  599.     });
    
  600.     let root;
    
  601.     await clientAct(async () => {
    
  602.       root = ReactDOMClient.hydrateRoot(container, <App />, {
    
  603.         identifierPrefix: 'custom-prefix-',
    
  604.       });
    
  605.     });
    
  606.     expect(container).toMatchInlineSnapshot(`
    
  607.       <div
    
  608.         id="container"
    
  609.       >
    
  610.         <div>
    
  611.           :custom-prefix-R1:
    
  612.         </div>
    
  613.         <div>
    
  614.           :custom-prefix-R2:
    
  615.         </div>
    
  616.       </div>
    
  617.     `);
    
  618. 
    
  619.     // Mount a new, client-only id
    
  620.     await clientAct(async () => {
    
  621.       root.render(<App showMore={true} />);
    
  622.     });
    
  623.     expect(container).toMatchInlineSnapshot(`
    
  624.       <div
    
  625.         id="container"
    
  626.       >
    
  627.         <div>
    
  628.           :custom-prefix-R1:
    
  629.         </div>
    
  630.         <div>
    
  631.           :custom-prefix-R2:
    
  632.         </div>
    
  633.         <div>
    
  634.           :custom-prefix-r0:
    
  635.         </div>
    
  636.       </div>
    
  637.     `);
    
  638.   });
    
  639. 
    
  640.   // https://github.com/vercel/next.js/issues/43033
    
  641.   // re-rendering in strict mode caused the localIdCounter to be reset but it the rerender hook does not
    
  642.   // increment it again. This only shows up as a problem for subsequent useId's because it affects child
    
  643.   // and sibling counters not the initial one
    
  644.   it('does not forget it mounted an id when re-rendering in dev', async () => {
    
  645.     function Parent() {
    
  646.       const id = useId();
    
  647.       return (
    
  648.         <div>
    
  649.           {id} <Child />
    
  650.         </div>
    
  651.       );
    
  652.     }
    
  653.     function Child() {
    
  654.       const id = useId();
    
  655.       return <div>{id}</div>;
    
  656.     }
    
  657. 
    
  658.     function App({showMore}) {
    
  659.       return (
    
  660.         <React.StrictMode>
    
  661.           <Parent />
    
  662.         </React.StrictMode>
    
  663.       );
    
  664.     }
    
  665. 
    
  666.     await serverAct(async () => {
    
  667.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
    
  668.       pipe(writable);
    
  669.     });
    
  670.     expect(container).toMatchInlineSnapshot(`
    
  671.       <div
    
  672.         id="container"
    
  673.       >
    
  674.         <div>
    
  675.           :R0:
    
  676.           <!-- -->
    
  677.            
    
  678.           <div>
    
  679.             :R7:
    
  680.           </div>
    
  681.         </div>
    
  682.       </div>
    
  683.     `);
    
  684. 
    
  685.     await clientAct(async () => {
    
  686.       ReactDOMClient.hydrateRoot(container, <App />);
    
  687.     });
    
  688.     expect(container).toMatchInlineSnapshot(`
    
  689.       <div
    
  690.         id="container"
    
  691.       >
    
  692.         <div>
    
  693.           :R0:
    
  694.           <!-- -->
    
  695.            
    
  696.           <div>
    
  697.             :R7:
    
  698.           </div>
    
  699.         </div>
    
  700.       </div>
    
  701.     `);
    
  702.   });
    
  703. });