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. 'use strict';
    
  12. 
    
  13. let JSDOM;
    
  14. let Stream;
    
  15. let React;
    
  16. let ReactDOM;
    
  17. let ReactDOMClient;
    
  18. let ReactDOMFizzServer;
    
  19. let document;
    
  20. let writable;
    
  21. let container;
    
  22. let buffer = '';
    
  23. let hasErrored = false;
    
  24. let fatalError = undefined;
    
  25. let waitForAll;
    
  26. 
    
  27. describe('ReactDOM HostSingleton', () => {
    
  28.   beforeEach(() => {
    
  29.     jest.resetModules();
    
  30.     JSDOM = require('jsdom').JSDOM;
    
  31.     React = require('react');
    
  32.     ReactDOM = require('react-dom');
    
  33.     ReactDOMClient = require('react-dom/client');
    
  34.     ReactDOMFizzServer = require('react-dom/server');
    
  35.     Stream = require('stream');
    
  36. 
    
  37.     const InternalTestUtils = require('internal-test-utils');
    
  38.     waitForAll = InternalTestUtils.waitForAll;
    
  39. 
    
  40.     // Test Environment
    
  41.     const jsdom = new JSDOM(
    
  42.       '<!DOCTYPE html><html><head></head><body><div id="container">',
    
  43.       {
    
  44.         runScripts: 'dangerously',
    
  45.       },
    
  46.     );
    
  47.     document = jsdom.window.document;
    
  48.     container = document.getElementById('container');
    
  49. 
    
  50.     buffer = '';
    
  51.     hasErrored = false;
    
  52. 
    
  53.     writable = new Stream.PassThrough();
    
  54.     writable.setEncoding('utf8');
    
  55.     writable.on('data', chunk => {
    
  56.       buffer += chunk;
    
  57.     });
    
  58.     writable.on('error', error => {
    
  59.       hasErrored = true;
    
  60.       fatalError = error;
    
  61.     });
    
  62.   });
    
  63. 
    
  64.   async function actIntoEmptyDocument(callback) {
    
  65.     await callback();
    
  66.     // Await one turn around the event loop.
    
  67.     // This assumes that we'll flush everything we have so far.
    
  68.     await new Promise(resolve => {
    
  69.       setImmediate(resolve);
    
  70.     });
    
  71.     if (hasErrored) {
    
  72.       throw fatalError;
    
  73.     }
    
  74. 
    
  75.     const bufferedContent = buffer;
    
  76.     buffer = '';
    
  77. 
    
  78.     const jsdom = new JSDOM(bufferedContent, {
    
  79.       runScripts: 'dangerously',
    
  80.     });
    
  81.     document = jsdom.window.document;
    
  82.     container = document;
    
  83.   }
    
  84. 
    
  85.   function getVisibleChildren(element) {
    
  86.     const children = [];
    
  87.     let node = element.firstChild;
    
  88.     while (node) {
    
  89.       if (node.nodeType === 1) {
    
  90.         const el: Element = (node: any);
    
  91.         if (
    
  92.           (el.tagName !== 'SCRIPT' &&
    
  93.             el.tagName !== 'TEMPLATE' &&
    
  94.             el.tagName !== 'template' &&
    
  95.             !el.hasAttribute('hidden') &&
    
  96.             !el.hasAttribute('aria-hidden')) ||
    
  97.           el.hasAttribute('data-meaningful')
    
  98.         ) {
    
  99.           const props = {};
    
  100.           const attributes = node.attributes;
    
  101.           for (let i = 0; i < attributes.length; i++) {
    
  102.             if (
    
  103.               attributes[i].name === 'id' &&
    
  104.               attributes[i].value.includes(':')
    
  105.             ) {
    
  106.               // We assume this is a React added ID that's a non-visual implementation detail.
    
  107.               continue;
    
  108.             }
    
  109.             props[attributes[i].name] = attributes[i].value;
    
  110.           }
    
  111.           props.children = getVisibleChildren(node);
    
  112.           children.push(React.createElement(node.tagName.toLowerCase(), props));
    
  113.         }
    
  114.       } else if (node.nodeType === 3) {
    
  115.         children.push(node.data);
    
  116.       }
    
  117.       node = node.nextSibling;
    
  118.     }
    
  119.     return children.length === 0
    
  120.       ? undefined
    
  121.       : children.length === 1
    
  122.       ? children[0]
    
  123.       : children;
    
  124.   }
    
  125. 
    
  126.   // @gate enableHostSingletons && enableFloat
    
  127.   it('warns if you render the same singleton twice at the same time', async () => {
    
  128.     const root = ReactDOMClient.createRoot(document);
    
  129.     root.render(
    
  130.       <html>
    
  131.         <head lang="en">
    
  132.           <title>Hello</title>
    
  133.         </head>
    
  134.         <body />
    
  135.       </html>,
    
  136.     );
    
  137.     await waitForAll([]);
    
  138.     expect(getVisibleChildren(document)).toEqual(
    
  139.       <html>
    
  140.         <head lang="en">
    
  141.           <title>Hello</title>
    
  142.         </head>
    
  143.         <body />
    
  144.       </html>,
    
  145.     );
    
  146.     root.render(
    
  147.       <html>
    
  148.         <head lang="en">
    
  149.           <title>Hello</title>
    
  150.         </head>
    
  151.         <head lang="es" data-foo="foo">
    
  152.           <title>Hola</title>
    
  153.         </head>
    
  154.         <body />
    
  155.       </html>,
    
  156.     );
    
  157.     await expect(async () => {
    
  158.       await waitForAll([]);
    
  159.     }).toErrorDev(
    
  160.       'Warning: You are mounting a new head component when a previous one has not first unmounted. It is an error to render more than one head component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <head> and if you need to mount a new one, ensure any previous ones have unmounted first',
    
  161.     );
    
  162.     expect(getVisibleChildren(document)).toEqual(
    
  163.       <html>
    
  164.         <head lang="es" data-foo="foo">
    
  165.           <title>Hola</title>
    
  166.           <title>Hello</title>
    
  167.         </head>
    
  168.         <body />
    
  169.       </html>,
    
  170.     );
    
  171. 
    
  172.     root.render(
    
  173.       <html>
    
  174.         {null}
    
  175.         {null}
    
  176.         <head lang="fr">
    
  177.           <title>Bonjour</title>
    
  178.         </head>
    
  179.         <body />
    
  180.       </html>,
    
  181.     );
    
  182.     await waitForAll([]);
    
  183.     expect(getVisibleChildren(document)).toEqual(
    
  184.       <html>
    
  185.         <head lang="fr">
    
  186.           <title>Bonjour</title>
    
  187.         </head>
    
  188.         <body />
    
  189.       </html>,
    
  190.     );
    
  191. 
    
  192.     root.render(
    
  193.       <html>
    
  194.         <head lang="en">
    
  195.           <title>Hello</title>
    
  196.         </head>
    
  197.         <body />
    
  198.       </html>,
    
  199.     );
    
  200.     await waitForAll([]);
    
  201.     expect(getVisibleChildren(document)).toEqual(
    
  202.       <html>
    
  203.         <head lang="en">
    
  204.           <title>Hello</title>
    
  205.         </head>
    
  206.         <body />
    
  207.       </html>,
    
  208.     );
    
  209.   });
    
  210. 
    
  211.   // @gate enableHostSingletons && enableFloat
    
  212.   it('renders into html, head, and body persistently so the node identities never change and extraneous styles are retained', async () => {
    
  213.     gate(flags => {
    
  214.       if (flags.enableHostSingletons !== true) {
    
  215.         // We throw here because when this test fails it ends up with sync work in a microtask
    
  216.         // that throws after the expectTestToFail check asserts the failure. this causes even the
    
  217.         // expected failure to fail. This just fails explicitly and early
    
  218.         throw new Error('manually opting out of test');
    
  219.       }
    
  220.     });
    
  221.     // Server render some html that will get replaced with a client render
    
  222.     await actIntoEmptyDocument(() => {
    
  223.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  224.         <html data-foo="foo">
    
  225.           <head data-bar="bar">
    
  226.             <link rel="stylesheet" href="resource" />
    
  227.             <title>a server title</title>
    
  228.             <link rel="stylesheet" href="3rdparty" />
    
  229.             <link rel="stylesheet" href="3rdparty2" />
    
  230.           </head>
    
  231.           <body data-baz="baz">
    
  232.             <div>hello world</div>
    
  233.             <style>
    
  234.               {`
    
  235.                 body: {
    
  236.                   background-color: red;
    
  237.                 }`}
    
  238.             </style>
    
  239.             <div>goodbye</div>
    
  240.           </body>
    
  241.         </html>,
    
  242.       );
    
  243.       pipe(writable);
    
  244.     });
    
  245.     expect(getVisibleChildren(document)).toEqual(
    
  246.       <html data-foo="foo">
    
  247.         <head data-bar="bar">
    
  248.           <title>a server title</title>
    
  249.           <link rel="stylesheet" href="resource" />
    
  250.           <link rel="stylesheet" href="3rdparty" />
    
  251.           <link rel="stylesheet" href="3rdparty2" />
    
  252.         </head>
    
  253.         <body data-baz="baz">
    
  254.           <div>hello world</div>
    
  255.           <style>
    
  256.             {`
    
  257.                 body: {
    
  258.                   background-color: red;
    
  259.                 }`}
    
  260.           </style>
    
  261.           <div>goodbye</div>
    
  262.         </body>
    
  263.       </html>,
    
  264.     );
    
  265.     const {documentElement, head, body} = document;
    
  266.     const persistentElements = [documentElement, head, body];
    
  267. 
    
  268.     // Render into the document completely different html. Observe that styles
    
  269.     // are retained as are html, body, and head referential identities. Because this was
    
  270.     // server rendered and we are not hydrating we lose the semantic placement of the original
    
  271.     // head contents and everything gets preprended. In a future update we might emit an insertion
    
  272.     // edge from the server and make client rendering reslilient to interstitial placement
    
  273.     const root = ReactDOMClient.createRoot(document);
    
  274.     root.render(
    
  275.       <html data-client-foo="foo">
    
  276.         <head>
    
  277.           <title>a client title</title>
    
  278.         </head>
    
  279.         <body data-client-baz="baz">
    
  280.           <div>hello client</div>
    
  281.         </body>
    
  282.       </html>,
    
  283.     );
    
  284.     await waitForAll([]);
    
  285.     expect(persistentElements).toEqual([
    
  286.       document.documentElement,
    
  287.       document.head,
    
  288.       document.body,
    
  289.     ]);
    
  290.     // Similar to Hydration we don't reset attributes on the instance itself even on a fresh render.
    
  291.     expect(getVisibleChildren(document)).toEqual(
    
  292.       <html data-client-foo="foo">
    
  293.         <head>
    
  294.           <link rel="stylesheet" href="resource" />
    
  295.           <link rel="stylesheet" href="3rdparty" />
    
  296.           <link rel="stylesheet" href="3rdparty2" />
    
  297.           <title>a client title</title>
    
  298.         </head>
    
  299.         <body data-client-baz="baz">
    
  300.           <style>
    
  301.             {`
    
  302.                 body: {
    
  303.                   background-color: red;
    
  304.                 }`}
    
  305.           </style>
    
  306.           <div>hello client</div>
    
  307.         </body>
    
  308.       </html>,
    
  309.     );
    
  310. 
    
  311.     // Render new children and assert they append in the correct locations
    
  312.     root.render(
    
  313.       <html data-client-foo="foo">
    
  314.         <head>
    
  315.           <title>a client title</title>
    
  316.           <meta />
    
  317.         </head>
    
  318.         <body data-client-baz="baz">
    
  319.           <p>hello client again</p>
    
  320.           <div>hello client</div>
    
  321.         </body>
    
  322.       </html>,
    
  323.     );
    
  324.     await waitForAll([]);
    
  325.     expect(persistentElements).toEqual([
    
  326.       document.documentElement,
    
  327.       document.head,
    
  328.       document.body,
    
  329.     ]);
    
  330.     expect(getVisibleChildren(document)).toEqual(
    
  331.       <html data-client-foo="foo">
    
  332.         <head>
    
  333.           <link rel="stylesheet" href="resource" />
    
  334.           <link rel="stylesheet" href="3rdparty" />
    
  335.           <link rel="stylesheet" href="3rdparty2" />
    
  336.           <title>a client title</title>
    
  337.           <meta />
    
  338.         </head>
    
  339.         <body data-client-baz="baz">
    
  340.           <style>
    
  341.             {`
    
  342.                 body: {
    
  343.                   background-color: red;
    
  344.                 }`}
    
  345.           </style>
    
  346.           <p>hello client again</p>
    
  347.           <div>hello client</div>
    
  348.         </body>
    
  349.       </html>,
    
  350.     );
    
  351. 
    
  352.     // Remove some children
    
  353.     root.render(
    
  354.       <html data-client-foo="foo">
    
  355.         <head>
    
  356.           <title>a client title</title>
    
  357.         </head>
    
  358.         <body data-client-baz="baz">
    
  359.           <p>hello client again</p>
    
  360.         </body>
    
  361.       </html>,
    
  362.     );
    
  363.     await waitForAll([]);
    
  364.     expect(persistentElements).toEqual([
    
  365.       document.documentElement,
    
  366.       document.head,
    
  367.       document.body,
    
  368.     ]);
    
  369.     expect(getVisibleChildren(document)).toEqual(
    
  370.       <html data-client-foo="foo">
    
  371.         <head>
    
  372.           <link rel="stylesheet" href="resource" />
    
  373.           <link rel="stylesheet" href="3rdparty" />
    
  374.           <link rel="stylesheet" href="3rdparty2" />
    
  375.           <title>a client title</title>
    
  376.         </head>
    
  377.         <body data-client-baz="baz">
    
  378.           <style>
    
  379.             {`
    
  380.                 body: {
    
  381.                   background-color: red;
    
  382.                 }`}
    
  383.           </style>
    
  384.           <p>hello client again</p>
    
  385.         </body>
    
  386.       </html>,
    
  387.     );
    
  388. 
    
  389.     // Remove a persistent component
    
  390.     // @TODO figure out whether to clean up attributes. restoring them is likely
    
  391.     // not possible.
    
  392.     root.render(
    
  393.       <html data-client-foo="foo">
    
  394.         <head>
    
  395.           <title>a client title</title>
    
  396.         </head>
    
  397.       </html>,
    
  398.     );
    
  399.     await waitForAll([]);
    
  400.     expect(persistentElements).toEqual([
    
  401.       document.documentElement,
    
  402.       document.head,
    
  403.       document.body,
    
  404.     ]);
    
  405.     expect(getVisibleChildren(document)).toEqual(
    
  406.       <html data-client-foo="foo">
    
  407.         <head>
    
  408.           <link rel="stylesheet" href="resource" />
    
  409.           <link rel="stylesheet" href="3rdparty" />
    
  410.           <link rel="stylesheet" href="3rdparty2" />
    
  411.           <title>a client title</title>
    
  412.         </head>
    
  413.         <body>
    
  414.           <style>
    
  415.             {`
    
  416.                 body: {
    
  417.                   background-color: red;
    
  418.                 }`}
    
  419.           </style>
    
  420.         </body>
    
  421.       </html>,
    
  422.     );
    
  423. 
    
  424.     // unmount the root
    
  425.     root.unmount();
    
  426.     await waitForAll([]);
    
  427.     expect(persistentElements).toEqual([
    
  428.       document.documentElement,
    
  429.       document.head,
    
  430.       document.body,
    
  431.     ]);
    
  432.     expect(getVisibleChildren(document)).toEqual(
    
  433.       <html>
    
  434.         <head>
    
  435.           <link rel="stylesheet" href="resource" />
    
  436.           <link rel="stylesheet" href="3rdparty" />
    
  437.           <link rel="stylesheet" href="3rdparty2" />
    
  438.         </head>
    
  439.         <body>
    
  440.           <style>
    
  441.             {`
    
  442.                 body: {
    
  443.                   background-color: red;
    
  444.                 }`}
    
  445.           </style>
    
  446.         </body>
    
  447.       </html>,
    
  448.     );
    
  449. 
    
  450.     // Now let's hydrate the document with known mismatching content
    
  451.     // We assert that the identities of html, head, and body still haven't changed
    
  452.     // and that the embedded styles are still retained
    
  453.     const hydrationErrors = [];
    
  454.     let hydrateRoot = ReactDOMClient.hydrateRoot(
    
  455.       document,
    
  456.       <html data-client-foo="foo">
    
  457.         <head>
    
  458.           <title>a client title</title>
    
  459.         </head>
    
  460.         <body data-client-baz="baz">
    
  461.           <div>hello client</div>
    
  462.         </body>
    
  463.       </html>,
    
  464.       {
    
  465.         onRecoverableError(error, errorInfo) {
    
  466.           hydrationErrors.push([
    
  467.             error.message,
    
  468.             errorInfo.componentStack
    
  469.               ? errorInfo.componentStack.split('\n')[1].trim()
    
  470.               : null,
    
  471.           ]);
    
  472.         },
    
  473.       },
    
  474.     );
    
  475.     await expect(async () => {
    
  476.       await waitForAll([]);
    
  477.     }).toErrorDev(
    
  478.       [
    
  479.         `Warning: Expected server HTML to contain a matching <div> in <body>.
    
  480.     in div (at **)
    
  481.     in body (at **)
    
  482.     in html (at **)`,
    
  483.         `Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.`,
    
  484.       ],
    
  485.       {withoutStack: 1},
    
  486.     );
    
  487.     expect(hydrationErrors).toEqual([
    
  488.       [
    
  489.         'Hydration failed because the initial UI does not match what was rendered on the server.',
    
  490.         'at div',
    
  491.       ],
    
  492.       [
    
  493.         'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
    
  494.         null,
    
  495.       ],
    
  496.     ]);
    
  497.     expect(persistentElements).toEqual([
    
  498.       document.documentElement,
    
  499.       document.head,
    
  500.       document.body,
    
  501.     ]);
    
  502.     expect(getVisibleChildren(document)).toEqual(
    
  503.       <html data-client-foo="foo">
    
  504.         <head>
    
  505.           <link rel="stylesheet" href="resource" />
    
  506.           <link rel="stylesheet" href="3rdparty" />
    
  507.           <link rel="stylesheet" href="3rdparty2" />
    
  508.           <title>a client title</title>
    
  509.         </head>
    
  510.         <body data-client-baz="baz">
    
  511.           <style>
    
  512.             {`
    
  513.                 body: {
    
  514.                   background-color: red;
    
  515.                 }`}
    
  516.           </style>
    
  517.           <div>hello client</div>
    
  518.         </body>
    
  519.       </html>,
    
  520.     );
    
  521. 
    
  522.     // Reset the tree
    
  523.     hydrationErrors.length = 0;
    
  524.     hydrateRoot.unmount();
    
  525. 
    
  526.     // Now we try hydrating again with matching nodes and we ensure
    
  527.     // the retained styles are bound to the hydrated fibers
    
  528.     const link = document.querySelector('link[rel="stylesheet"]');
    
  529.     const style = document.querySelector('style');
    
  530.     hydrateRoot = ReactDOMClient.hydrateRoot(
    
  531.       document,
    
  532.       <html data-client-foo="foo">
    
  533.         <head>
    
  534.           <link rel="stylesheet" href="resource" />
    
  535.           <link rel="stylesheet" href="3rdparty" />
    
  536.           <link rel="stylesheet" href="3rdparty2" />
    
  537.         </head>
    
  538.         <body data-client-baz="baz">
    
  539.           <style>
    
  540.             {`
    
  541.                 body: {
    
  542.                   background-color: red;
    
  543.                 }`}
    
  544.           </style>
    
  545.         </body>
    
  546.       </html>,
    
  547.       {
    
  548.         onRecoverableError(error, errorInfo) {
    
  549.           hydrationErrors.push([
    
  550.             error.message,
    
  551.             errorInfo.componentStack
    
  552.               ? errorInfo.componentStack.split('\n')[1].trim()
    
  553.               : null,
    
  554.           ]);
    
  555.         },
    
  556.       },
    
  557.     );
    
  558.     expect(hydrationErrors).toEqual([]);
    
  559.     await waitForAll([]);
    
  560.     expect(persistentElements).toEqual([
    
  561.       document.documentElement,
    
  562.       document.head,
    
  563.       document.body,
    
  564.     ]);
    
  565.     expect([link, style]).toEqual([
    
  566.       document.querySelector('link[rel="stylesheet"]'),
    
  567.       document.querySelector('style'),
    
  568.     ]);
    
  569.     expect(getVisibleChildren(document)).toEqual(
    
  570.       <html>
    
  571.         <head>
    
  572.           <link rel="stylesheet" href="resource" />
    
  573.           <link rel="stylesheet" href="3rdparty" />
    
  574.           <link rel="stylesheet" href="3rdparty2" />
    
  575.         </head>
    
  576.         <body>
    
  577.           <style>
    
  578.             {`
    
  579.                 body: {
    
  580.                   background-color: red;
    
  581.                 }`}
    
  582.           </style>
    
  583.         </body>
    
  584.       </html>,
    
  585.     );
    
  586. 
    
  587.     // We unmount a final time and observe that still we retain our persistent nodes
    
  588.     // but they style contents which matched in hydration is removed
    
  589.     hydrateRoot.unmount();
    
  590.     expect(persistentElements).toEqual([
    
  591.       document.documentElement,
    
  592.       document.head,
    
  593.       document.body,
    
  594.     ]);
    
  595.     expect(getVisibleChildren(document)).toEqual(
    
  596.       <html>
    
  597.         <head />
    
  598.         <body />
    
  599.       </html>,
    
  600.     );
    
  601.   });
    
  602. 
    
  603.   // This test is not supported in this implementation. If we reintroduce insertion edge we should revisit
    
  604.   // @gate enableHostSingletons
    
  605.   xit('is able to maintain insertions in head and body between tree-adjacent Nodes', async () => {
    
  606.     // Server render some html and hydrate on the client
    
  607.     await actIntoEmptyDocument(() => {
    
  608.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  609.         <html>
    
  610.           <head>
    
  611.             <title>title</title>
    
  612.           </head>
    
  613.           <body>
    
  614.             <div>hello</div>
    
  615.           </body>
    
  616.         </html>,
    
  617.       );
    
  618.       pipe(writable);
    
  619.     });
    
  620.     const root = ReactDOMClient.hydrateRoot(
    
  621.       document,
    
  622.       <html>
    
  623.         <head>
    
  624.           <title>title</title>
    
  625.         </head>
    
  626.         <body>
    
  627.           <div>hello</div>
    
  628.         </body>
    
  629.       </html>,
    
  630.     );
    
  631.     await waitForAll([]);
    
  632. 
    
  633.     // We construct and insert some artificial stylesheets mimicing what a 3rd party script might do
    
  634.     // In the future we could hydrate with these already in the document but the rules are restrictive
    
  635.     // still so it would fail and fall back to client rendering
    
  636.     const [a, b, c, d, e, f, g, h] = 'abcdefgh'.split('').map(letter => {
    
  637.       const link = document.createElement('link');
    
  638.       link.rel = 'stylesheet';
    
  639.       link.href = letter;
    
  640.       return link;
    
  641.     });
    
  642. 
    
  643.     const head = document.head;
    
  644.     const title = head.firstChild;
    
  645.     head.insertBefore(a, title);
    
  646.     head.insertBefore(b, title);
    
  647.     head.appendChild(c);
    
  648.     head.appendChild(d);
    
  649. 
    
  650.     const bodyContent = document.body.firstChild;
    
  651.     const body = document.body;
    
  652.     body.insertBefore(e, bodyContent);
    
  653.     body.insertBefore(f, bodyContent);
    
  654.     body.appendChild(g);
    
  655.     body.appendChild(h);
    
  656. 
    
  657.     expect(getVisibleChildren(document)).toEqual(
    
  658.       <html>
    
  659.         <head>
    
  660.           <link rel="stylesheet" href="a" />
    
  661.           <link rel="stylesheet" href="b" />
    
  662.           <title>title</title>
    
  663.           <link rel="stylesheet" href="c" />
    
  664.           <link rel="stylesheet" href="d" />
    
  665.         </head>
    
  666.         <body>
    
  667.           <link rel="stylesheet" href="e" />
    
  668.           <link rel="stylesheet" href="f" />
    
  669.           <div>hello</div>
    
  670.           <link rel="stylesheet" href="g" />
    
  671.           <link rel="stylesheet" href="h" />
    
  672.         </body>
    
  673.       </html>,
    
  674.     );
    
  675. 
    
  676.     // Unmount head and change children of body
    
  677.     root.render(
    
  678.       <html>
    
  679.         {null}
    
  680.         <body>
    
  681.           <div>hello</div>
    
  682.           <div>world</div>
    
  683.         </body>
    
  684.       </html>,
    
  685.     );
    
  686. 
    
  687.     await waitForAll([]);
    
  688.     expect(getVisibleChildren(document)).toEqual(
    
  689.       <html>
    
  690.         <head>
    
  691.           <link rel="stylesheet" href="a" />
    
  692.           <link rel="stylesheet" href="b" />
    
  693.           <link rel="stylesheet" href="c" />
    
  694.           <link rel="stylesheet" href="d" />
    
  695.         </head>
    
  696.         <body>
    
  697.           <link rel="stylesheet" href="e" />
    
  698.           <link rel="stylesheet" href="f" />
    
  699.           <div>hello</div>
    
  700.           <div>world</div>
    
  701.           <link rel="stylesheet" href="g" />
    
  702.           <link rel="stylesheet" href="h" />
    
  703.         </body>
    
  704.       </html>,
    
  705.     );
    
  706. 
    
  707.     // Mount new head and unmount body
    
  708.     root.render(
    
  709.       <html>
    
  710.         <head>
    
  711.           <title>a new title</title>
    
  712.         </head>
    
  713.       </html>,
    
  714.     );
    
  715.     await waitForAll([]);
    
  716.     expect(getVisibleChildren(document)).toEqual(
    
  717.       <html>
    
  718.         <head>
    
  719.           <title>a new title</title>
    
  720.           <link rel="stylesheet" href="a" />
    
  721.           <link rel="stylesheet" href="b" />
    
  722.           <link rel="stylesheet" href="c" />
    
  723.           <link rel="stylesheet" href="d" />
    
  724.         </head>
    
  725.         <body>
    
  726.           <link rel="stylesheet" href="e" />
    
  727.           <link rel="stylesheet" href="f" />
    
  728.           <link rel="stylesheet" href="g" />
    
  729.           <link rel="stylesheet" href="h" />
    
  730.         </body>
    
  731.       </html>,
    
  732.     );
    
  733.   });
    
  734. 
    
  735.   // @gate enableHostSingletons
    
  736.   it('clears persistent head and body when html is the container', async () => {
    
  737.     await actIntoEmptyDocument(() => {
    
  738.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  739.         <html>
    
  740.           <head>
    
  741.             <link rel="stylesheet" href="headbefore" />
    
  742.             <title>this should be removed</title>
    
  743.             <link rel="stylesheet" href="headafter" />
    
  744.             <script data-meaningful="">true</script>
    
  745.           </head>
    
  746.           <body>
    
  747.             <link rel="stylesheet" href="bodybefore" />
    
  748.             <div>this should be removed</div>
    
  749.             <link rel="stylesheet" href="bodyafter" />
    
  750.             <script data-meaningful="">true</script>
    
  751.           </body>
    
  752.         </html>,
    
  753.       );
    
  754.       pipe(writable);
    
  755.     });
    
  756.     container = document.documentElement;
    
  757. 
    
  758.     const root = ReactDOMClient.createRoot(container);
    
  759.     root.render(
    
  760.       <>
    
  761.         <head>
    
  762.           <title>something new</title>
    
  763.         </head>
    
  764.         <body>
    
  765.           <div>something new</div>
    
  766.         </body>
    
  767.       </>,
    
  768.     );
    
  769.     await waitForAll([]);
    
  770.     expect(getVisibleChildren(document)).toEqual(
    
  771.       <html>
    
  772.         <head>
    
  773.           <link rel="stylesheet" href="headbefore" />
    
  774.           <link rel="stylesheet" href="headafter" />
    
  775.           <script data-meaningful="">true</script>
    
  776.           <title>something new</title>
    
  777.         </head>
    
  778.         <body>
    
  779.           <link rel="stylesheet" href="bodybefore" />
    
  780.           <link rel="stylesheet" href="bodyafter" />
    
  781.           <script data-meaningful="">true</script>
    
  782.           <div>something new</div>
    
  783.         </body>
    
  784.       </html>,
    
  785.     );
    
  786.   });
    
  787. 
    
  788.   // @gate enableHostSingletons
    
  789.   it('clears persistent head when it is the container', async () => {
    
  790.     await actIntoEmptyDocument(() => {
    
  791.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  792.         <html>
    
  793.           <head>
    
  794.             <link rel="stylesheet" href="before" />
    
  795.             <title>this should be removed</title>
    
  796.             <link rel="stylesheet" href="after" />
    
  797.           </head>
    
  798.           <body />
    
  799.         </html>,
    
  800.       );
    
  801.       pipe(writable);
    
  802.     });
    
  803.     container = document.head;
    
  804. 
    
  805.     const root = ReactDOMClient.createRoot(container);
    
  806.     root.render(<title>something new</title>);
    
  807.     await waitForAll([]);
    
  808.     expect(getVisibleChildren(document)).toEqual(
    
  809.       <html>
    
  810.         <head>
    
  811.           <link rel="stylesheet" href="before" />
    
  812.           <link rel="stylesheet" href="after" />
    
  813.           <title>something new</title>
    
  814.         </head>
    
  815.         <body />
    
  816.       </html>,
    
  817.     );
    
  818.   });
    
  819. 
    
  820.   // @gate enableHostSingletons && enableFloat
    
  821.   it('clears persistent body when it is the container', async () => {
    
  822.     await actIntoEmptyDocument(() => {
    
  823.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  824.         <html>
    
  825.           <head />
    
  826.           <body>
    
  827.             <link rel="stylesheet" href="before" />
    
  828.             <div>this should be removed</div>
    
  829.             <link rel="stylesheet" href="after" />
    
  830.           </body>
    
  831.         </html>,
    
  832.       );
    
  833.       pipe(writable);
    
  834.     });
    
  835.     container = document.body;
    
  836. 
    
  837.     const root = ReactDOMClient.createRoot(container);
    
  838.     root.render(<div>something new</div>);
    
  839.     await waitForAll([]);
    
  840.     expect(getVisibleChildren(document)).toEqual(
    
  841.       <html>
    
  842.         <head />
    
  843.         <body>
    
  844.           <link rel="stylesheet" href="before" />
    
  845.           <link rel="stylesheet" href="after" />
    
  846.           <div>something new</div>
    
  847.         </body>
    
  848.       </html>,
    
  849.     );
    
  850.   });
    
  851. 
    
  852.   it('renders single Text children into HostSingletons correctly', async () => {
    
  853.     await actIntoEmptyDocument(() => {
    
  854.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  855.         <html>
    
  856.           <head />
    
  857.           <body>foo</body>
    
  858.         </html>,
    
  859.       );
    
  860.       pipe(writable);
    
  861.     });
    
  862. 
    
  863.     let root = ReactDOMClient.hydrateRoot(
    
  864.       document,
    
  865.       <html>
    
  866.         <head />
    
  867.         <body>foo</body>
    
  868.       </html>,
    
  869.     );
    
  870.     await waitForAll([]);
    
  871.     expect(getVisibleChildren(document)).toEqual(
    
  872.       <html>
    
  873.         <head />
    
  874.         <body>foo</body>
    
  875.       </html>,
    
  876.     );
    
  877. 
    
  878.     root.render(
    
  879.       <html>
    
  880.         <head />
    
  881.         <body>bar</body>
    
  882.       </html>,
    
  883.     );
    
  884.     await waitForAll([]);
    
  885.     expect(getVisibleChildren(document)).toEqual(
    
  886.       <html>
    
  887.         <head />
    
  888.         <body>bar</body>
    
  889.       </html>,
    
  890.     );
    
  891. 
    
  892.     root.unmount();
    
  893. 
    
  894.     root = ReactDOMClient.createRoot(document);
    
  895.     root.render(
    
  896.       <html>
    
  897.         <head />
    
  898.         <body>baz</body>
    
  899.       </html>,
    
  900.     );
    
  901.     await waitForAll([]);
    
  902.     expect(getVisibleChildren(document)).toEqual(
    
  903.       <html>
    
  904.         <head />
    
  905.         <body>baz</body>
    
  906.       </html>,
    
  907.     );
    
  908.   });
    
  909. 
    
  910.   it('supports going from single text child to many children back to single text child in body', async () => {
    
  911.     const root = ReactDOMClient.createRoot(document);
    
  912.     root.render(
    
  913.       <html>
    
  914.         <head />
    
  915.         <body>foo</body>
    
  916.       </html>,
    
  917.     );
    
  918.     await waitForAll([]);
    
  919.     expect(getVisibleChildren(document)).toEqual(
    
  920.       <html>
    
  921.         <head />
    
  922.         <body>foo</body>
    
  923.       </html>,
    
  924.     );
    
  925. 
    
  926.     root.render(
    
  927.       <html>
    
  928.         <head />
    
  929.         <body>
    
  930.           <div>foo</div>
    
  931.         </body>
    
  932.       </html>,
    
  933.     );
    
  934.     await waitForAll([]);
    
  935.     expect(getVisibleChildren(document)).toEqual(
    
  936.       <html>
    
  937.         <head />
    
  938.         <body>
    
  939.           <div>foo</div>
    
  940.         </body>
    
  941.       </html>,
    
  942.     );
    
  943. 
    
  944.     root.render(
    
  945.       <html>
    
  946.         <head />
    
  947.         <body>foo</body>
    
  948.       </html>,
    
  949.     );
    
  950.     await waitForAll([]);
    
  951.     expect(getVisibleChildren(document)).toEqual(
    
  952.       <html>
    
  953.         <head />
    
  954.         <body>foo</body>
    
  955.       </html>,
    
  956.     );
    
  957. 
    
  958.     root.render(
    
  959.       <html>
    
  960.         <head />
    
  961.         <body>
    
  962.           <div>foo</div>
    
  963.         </body>
    
  964.       </html>,
    
  965.     );
    
  966.     await waitForAll([]);
    
  967.     expect(getVisibleChildren(document)).toEqual(
    
  968.       <html>
    
  969.         <head />
    
  970.         <body>
    
  971.           <div>foo</div>
    
  972.         </body>
    
  973.       </html>,
    
  974.     );
    
  975.   });
    
  976. 
    
  977.   // @gate enableHostSingletons
    
  978.   it('allows for hydrating without a head', async () => {
    
  979.     await actIntoEmptyDocument(() => {
    
  980.       const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
    
  981.         <html>
    
  982.           <body>foo</body>
    
  983.         </html>,
    
  984.       );
    
  985.       pipe(writable);
    
  986.     });
    
  987. 
    
  988.     expect(getVisibleChildren(document)).toEqual(
    
  989.       <html>
    
  990.         <head />
    
  991.         <body>foo</body>
    
  992.       </html>,
    
  993.     );
    
  994. 
    
  995.     ReactDOMClient.hydrateRoot(
    
  996.       document,
    
  997.       <html>
    
  998.         <body>foo</body>
    
  999.       </html>,
    
  1000.     );
    
  1001.     await waitForAll([]);
    
  1002.     expect(getVisibleChildren(document)).toEqual(
    
  1003.       <html>
    
  1004.         <head />
    
  1005.         <body>foo</body>
    
  1006.       </html>,
    
  1007.     );
    
  1008.   });
    
  1009. 
    
  1010.   // https://github.com/facebook/react/issues/26128
    
  1011.   it('(#26128) does not throw when rendering at body', async () => {
    
  1012.     ReactDOM.render(<div />, document.body);
    
  1013.   });
    
  1014. 
    
  1015.   // https://github.com/facebook/react/issues/26128
    
  1016.   it('(#26128) does not throw when rendering at <html>', async () => {
    
  1017.     ReactDOM.render(<body />, document.documentElement);
    
  1018.   });
    
  1019. 
    
  1020.   // https://github.com/facebook/react/issues/26128
    
  1021.   it('(#26128) does not throw when rendering at document', async () => {
    
  1022.     ReactDOM.render(<html />, document);
    
  1023.   });
    
  1024. });