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.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. import {
    
  13.   getVisibleChildren,
    
  14.   insertNodesAndExecuteScripts,
    
  15. } from '../test-utils/FizzTestUtils';
    
  16. 
    
  17. // Polyfills for test environment
    
  18. global.ReadableStream =
    
  19.   require('web-streams-polyfill/ponyfill/es6').ReadableStream;
    
  20. global.TextEncoder = require('util').TextEncoder;
    
  21. 
    
  22. let React;
    
  23. let ReactDOM;
    
  24. let ReactDOMFizzServer;
    
  25. let ReactDOMFizzStatic;
    
  26. let Suspense;
    
  27. let container;
    
  28. 
    
  29. describe('ReactDOMFizzStaticBrowser', () => {
    
  30.   beforeEach(() => {
    
  31.     jest.resetModules();
    
  32.     React = require('react');
    
  33.     ReactDOM = require('react-dom');
    
  34.     ReactDOMFizzServer = require('react-dom/server.browser');
    
  35.     if (__EXPERIMENTAL__) {
    
  36.       ReactDOMFizzStatic = require('react-dom/static.browser');
    
  37.     }
    
  38.     Suspense = React.Suspense;
    
  39.     container = document.createElement('div');
    
  40.     document.body.appendChild(container);
    
  41.   });
    
  42. 
    
  43.   afterEach(() => {
    
  44.     document.body.removeChild(container);
    
  45.   });
    
  46. 
    
  47.   const theError = new Error('This is an error');
    
  48.   function Throw() {
    
  49.     throw theError;
    
  50.   }
    
  51.   const theInfinitePromise = new Promise(() => {});
    
  52.   function InfiniteSuspend() {
    
  53.     throw theInfinitePromise;
    
  54.   }
    
  55. 
    
  56.   function concat(streamA, streamB) {
    
  57.     const readerA = streamA.getReader();
    
  58.     const readerB = streamB.getReader();
    
  59.     return new ReadableStream({
    
  60.       start(controller) {
    
  61.         function readA() {
    
  62.           readerA.read().then(({done, value}) => {
    
  63.             if (done) {
    
  64.               readB();
    
  65.               return;
    
  66.             }
    
  67.             controller.enqueue(value);
    
  68.             readA();
    
  69.           });
    
  70.         }
    
  71.         function readB() {
    
  72.           readerB.read().then(({done, value}) => {
    
  73.             if (done) {
    
  74.               controller.close();
    
  75.               return;
    
  76.             }
    
  77.             controller.enqueue(value);
    
  78.             readB();
    
  79.           });
    
  80.         }
    
  81.         readA();
    
  82.       },
    
  83.     });
    
  84.   }
    
  85. 
    
  86.   async function readContent(stream) {
    
  87.     const reader = stream.getReader();
    
  88.     let content = '';
    
  89.     while (true) {
    
  90.       const {done, value} = await reader.read();
    
  91.       if (done) {
    
  92.         return content;
    
  93.       }
    
  94.       content += Buffer.from(value).toString('utf8');
    
  95.     }
    
  96.   }
    
  97. 
    
  98.   async function readIntoContainer(stream) {
    
  99.     const reader = stream.getReader();
    
  100.     let result = '';
    
  101.     while (true) {
    
  102.       const {done, value} = await reader.read();
    
  103.       if (done) {
    
  104.         break;
    
  105.       }
    
  106.       result += Buffer.from(value).toString('utf8');
    
  107.     }
    
  108.     const temp = document.createElement('div');
    
  109.     temp.innerHTML = result;
    
  110.     await insertNodesAndExecuteScripts(temp, container, null);
    
  111.   }
    
  112. 
    
  113.   // @gate experimental
    
  114.   it('should call prerender', async () => {
    
  115.     const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>);
    
  116.     const prelude = await readContent(result.prelude);
    
  117.     expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
    
  118.   });
    
  119. 
    
  120.   // @gate experimental
    
  121.   it('should emit DOCTYPE at the root of the document', async () => {
    
  122.     const result = await ReactDOMFizzStatic.prerender(
    
  123.       <html>
    
  124.         <body>hello world</body>
    
  125.       </html>,
    
  126.     );
    
  127.     const prelude = await readContent(result.prelude);
    
  128.     if (gate(flags => flags.enableFloat)) {
    
  129.       expect(prelude).toMatchInlineSnapshot(
    
  130.         `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
    
  131.       );
    
  132.     } else {
    
  133.       expect(prelude).toMatchInlineSnapshot(
    
  134.         `"<!DOCTYPE html><html><body>hello world</body></html>"`,
    
  135.       );
    
  136.     }
    
  137.   });
    
  138. 
    
  139.   // @gate experimental
    
  140.   it('should emit bootstrap script src at the end', async () => {
    
  141.     const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>, {
    
  142.       bootstrapScriptContent: 'INIT();',
    
  143.       bootstrapScripts: ['init.js'],
    
  144.       bootstrapModules: ['init.mjs'],
    
  145.     });
    
  146.     const prelude = await readContent(result.prelude);
    
  147.     expect(prelude).toMatchInlineSnapshot(
    
  148.       `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
    
  149.     );
    
  150.   });
    
  151. 
    
  152.   // @gate experimental
    
  153.   it('emits all HTML as one unit', async () => {
    
  154.     let hasLoaded = false;
    
  155.     let resolve;
    
  156.     const promise = new Promise(r => (resolve = r));
    
  157.     function Wait() {
    
  158.       if (!hasLoaded) {
    
  159.         throw promise;
    
  160.       }
    
  161.       return 'Done';
    
  162.     }
    
  163.     const resultPromise = ReactDOMFizzStatic.prerender(
    
  164.       <div>
    
  165.         <Suspense fallback="Loading">
    
  166.           <Wait />
    
  167.         </Suspense>
    
  168.       </div>,
    
  169.     );
    
  170. 
    
  171.     await jest.runAllTimers();
    
  172. 
    
  173.     // Resolve the loading.
    
  174.     hasLoaded = true;
    
  175.     await resolve();
    
  176. 
    
  177.     const result = await resultPromise;
    
  178.     const prelude = await readContent(result.prelude);
    
  179.     expect(prelude).toMatchInlineSnapshot(
    
  180.       `"<div><!--$-->Done<!-- --><!--/$--></div>"`,
    
  181.     );
    
  182.   });
    
  183. 
    
  184.   // @gate experimental
    
  185.   it('should reject the promise when an error is thrown at the root', async () => {
    
  186.     const reportedErrors = [];
    
  187.     let caughtError = null;
    
  188.     try {
    
  189.       await ReactDOMFizzStatic.prerender(
    
  190.         <div>
    
  191.           <Throw />
    
  192.         </div>,
    
  193.         {
    
  194.           onError(x) {
    
  195.             reportedErrors.push(x);
    
  196.           },
    
  197.         },
    
  198.       );
    
  199.     } catch (error) {
    
  200.       caughtError = error;
    
  201.     }
    
  202.     expect(caughtError).toBe(theError);
    
  203.     expect(reportedErrors).toEqual([theError]);
    
  204.   });
    
  205. 
    
  206.   // @gate experimental
    
  207.   it('should reject the promise when an error is thrown inside a fallback', async () => {
    
  208.     const reportedErrors = [];
    
  209.     let caughtError = null;
    
  210.     try {
    
  211.       await ReactDOMFizzStatic.prerender(
    
  212.         <div>
    
  213.           <Suspense fallback={<Throw />}>
    
  214.             <InfiniteSuspend />
    
  215.           </Suspense>
    
  216.         </div>,
    
  217.         {
    
  218.           onError(x) {
    
  219.             reportedErrors.push(x);
    
  220.           },
    
  221.         },
    
  222.       );
    
  223.     } catch (error) {
    
  224.       caughtError = error;
    
  225.     }
    
  226.     expect(caughtError).toBe(theError);
    
  227.     expect(reportedErrors).toEqual([theError]);
    
  228.   });
    
  229. 
    
  230.   // @gate experimental
    
  231.   it('should not error the stream when an error is thrown inside suspense boundary', async () => {
    
  232.     const reportedErrors = [];
    
  233.     const result = await ReactDOMFizzStatic.prerender(
    
  234.       <div>
    
  235.         <Suspense fallback={<div>Loading</div>}>
    
  236.           <Throw />
    
  237.         </Suspense>
    
  238.       </div>,
    
  239.       {
    
  240.         onError(x) {
    
  241.           reportedErrors.push(x);
    
  242.         },
    
  243.       },
    
  244.     );
    
  245. 
    
  246.     const prelude = await readContent(result.prelude);
    
  247.     expect(prelude).toContain('Loading');
    
  248.     expect(reportedErrors).toEqual([theError]);
    
  249.   });
    
  250. 
    
  251.   // @gate experimental
    
  252.   it('should be able to complete by aborting even if the promise never resolves', async () => {
    
  253.     const errors = [];
    
  254.     const controller = new AbortController();
    
  255.     const resultPromise = ReactDOMFizzStatic.prerender(
    
  256.       <div>
    
  257.         <Suspense fallback={<div>Loading</div>}>
    
  258.           <InfiniteSuspend />
    
  259.         </Suspense>
    
  260.       </div>,
    
  261.       {
    
  262.         signal: controller.signal,
    
  263.         onError(x) {
    
  264.           errors.push(x.message);
    
  265.         },
    
  266.       },
    
  267.     );
    
  268. 
    
  269.     await jest.runAllTimers();
    
  270. 
    
  271.     controller.abort();
    
  272. 
    
  273.     const result = await resultPromise;
    
  274. 
    
  275.     const prelude = await readContent(result.prelude);
    
  276.     expect(prelude).toContain('Loading');
    
  277. 
    
  278.     expect(errors).toEqual(['The operation was aborted.']);
    
  279.   });
    
  280. 
    
  281.   // @gate experimental
    
  282.   it('should reject if aborting before the shell is complete', async () => {
    
  283.     const errors = [];
    
  284.     const controller = new AbortController();
    
  285.     const promise = ReactDOMFizzStatic.prerender(
    
  286.       <div>
    
  287.         <InfiniteSuspend />
    
  288.       </div>,
    
  289.       {
    
  290.         signal: controller.signal,
    
  291.         onError(x) {
    
  292.           errors.push(x.message);
    
  293.         },
    
  294.       },
    
  295.     );
    
  296. 
    
  297.     await jest.runAllTimers();
    
  298. 
    
  299.     const theReason = new Error('aborted for reasons');
    
  300.     controller.abort(theReason);
    
  301. 
    
  302.     let caughtError = null;
    
  303.     try {
    
  304.       await promise;
    
  305.     } catch (error) {
    
  306.       caughtError = error;
    
  307.     }
    
  308.     expect(caughtError).toBe(theReason);
    
  309.     expect(errors).toEqual(['aborted for reasons']);
    
  310.   });
    
  311. 
    
  312.   // @gate experimental
    
  313.   it('should be able to abort before something suspends', async () => {
    
  314.     const errors = [];
    
  315.     const controller = new AbortController();
    
  316.     function App() {
    
  317.       controller.abort();
    
  318.       return (
    
  319.         <Suspense fallback={<div>Loading</div>}>
    
  320.           <InfiniteSuspend />
    
  321.         </Suspense>
    
  322.       );
    
  323.     }
    
  324.     const streamPromise = ReactDOMFizzStatic.prerender(
    
  325.       <div>
    
  326.         <App />
    
  327.       </div>,
    
  328.       {
    
  329.         signal: controller.signal,
    
  330.         onError(x) {
    
  331.           errors.push(x.message);
    
  332.         },
    
  333.       },
    
  334.     );
    
  335. 
    
  336.     let caughtError = null;
    
  337.     try {
    
  338.       await streamPromise;
    
  339.     } catch (error) {
    
  340.       caughtError = error;
    
  341.     }
    
  342.     expect(caughtError.message).toBe('The operation was aborted.');
    
  343.     expect(errors).toEqual(['The operation was aborted.']);
    
  344.   });
    
  345. 
    
  346.   // @gate experimental
    
  347.   it('should reject if passing an already aborted signal', async () => {
    
  348.     const errors = [];
    
  349.     const controller = new AbortController();
    
  350.     const theReason = new Error('aborted for reasons');
    
  351.     controller.abort(theReason);
    
  352. 
    
  353.     const promise = ReactDOMFizzStatic.prerender(
    
  354.       <div>
    
  355.         <Suspense fallback={<div>Loading</div>}>
    
  356.           <InfiniteSuspend />
    
  357.         </Suspense>
    
  358.       </div>,
    
  359.       {
    
  360.         signal: controller.signal,
    
  361.         onError(x) {
    
  362.           errors.push(x.message);
    
  363.         },
    
  364.       },
    
  365.     );
    
  366. 
    
  367.     // Technically we could still continue rendering the shell but currently the
    
  368.     // semantics mean that we also abort any pending CPU work.
    
  369.     let caughtError = null;
    
  370.     try {
    
  371.       await promise;
    
  372.     } catch (error) {
    
  373.       caughtError = error;
    
  374.     }
    
  375.     expect(caughtError).toBe(theReason);
    
  376.     expect(errors).toEqual(['aborted for reasons']);
    
  377.   });
    
  378. 
    
  379.   // @gate experimental
    
  380.   it('supports custom abort reasons with a string', async () => {
    
  381.     const promise = new Promise(r => {});
    
  382.     function Wait() {
    
  383.       throw promise;
    
  384.     }
    
  385.     function App() {
    
  386.       return (
    
  387.         <div>
    
  388.           <p>
    
  389.             <Suspense fallback={'p'}>
    
  390.               <Wait />
    
  391.             </Suspense>
    
  392.           </p>
    
  393.           <span>
    
  394.             <Suspense fallback={'span'}>
    
  395.               <Wait />
    
  396.             </Suspense>
    
  397.           </span>
    
  398.         </div>
    
  399.       );
    
  400.     }
    
  401. 
    
  402.     const errors = [];
    
  403.     const controller = new AbortController();
    
  404.     const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
    
  405.       signal: controller.signal,
    
  406.       onError(x) {
    
  407.         errors.push(x);
    
  408.         return 'a digest';
    
  409.       },
    
  410.     });
    
  411. 
    
  412.     controller.abort('foobar');
    
  413. 
    
  414.     await resultPromise;
    
  415. 
    
  416.     expect(errors).toEqual(['foobar', 'foobar']);
    
  417.   });
    
  418. 
    
  419.   // @gate experimental
    
  420.   it('supports custom abort reasons with an Error', async () => {
    
  421.     const promise = new Promise(r => {});
    
  422.     function Wait() {
    
  423.       throw promise;
    
  424.     }
    
  425.     function App() {
    
  426.       return (
    
  427.         <div>
    
  428.           <p>
    
  429.             <Suspense fallback={'p'}>
    
  430.               <Wait />
    
  431.             </Suspense>
    
  432.           </p>
    
  433.           <span>
    
  434.             <Suspense fallback={'span'}>
    
  435.               <Wait />
    
  436.             </Suspense>
    
  437.           </span>
    
  438.         </div>
    
  439.       );
    
  440.     }
    
  441. 
    
  442.     const errors = [];
    
  443.     const controller = new AbortController();
    
  444.     const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
    
  445.       signal: controller.signal,
    
  446.       onError(x) {
    
  447.         errors.push(x.message);
    
  448.         return 'a digest';
    
  449.       },
    
  450.     });
    
  451. 
    
  452.     controller.abort(new Error('uh oh'));
    
  453. 
    
  454.     await resultPromise;
    
  455. 
    
  456.     expect(errors).toEqual(['uh oh', 'uh oh']);
    
  457.   });
    
  458. 
    
  459.   // @gate enablePostpone
    
  460.   it('supports postponing in prerender and resuming later', async () => {
    
  461.     let prerendering = true;
    
  462.     function Postpone() {
    
  463.       if (prerendering) {
    
  464.         React.unstable_postpone();
    
  465.       }
    
  466.       return ['Hello', 'World'];
    
  467.     }
    
  468. 
    
  469.     function App() {
    
  470.       return (
    
  471.         <div>
    
  472.           <Suspense fallback="Loading...">
    
  473.             <Postpone />
    
  474.           </Suspense>
    
  475.         </div>
    
  476.       );
    
  477.     }
    
  478. 
    
  479.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  480.     expect(prerendered.postponed).not.toBe(null);
    
  481. 
    
  482.     prerendering = false;
    
  483. 
    
  484.     const resumed = await ReactDOMFizzServer.resume(
    
  485.       <App />,
    
  486.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  487.     );
    
  488. 
    
  489.     await readIntoContainer(prerendered.prelude);
    
  490. 
    
  491.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  492. 
    
  493.     await readIntoContainer(resumed);
    
  494. 
    
  495.     expect(getVisibleChildren(container)).toEqual(
    
  496.       <div>{['Hello', 'World']}</div>,
    
  497.     );
    
  498.   });
    
  499. 
    
  500.   // @gate enablePostpone
    
  501.   it('supports postponing in prerender and resuming with a prefix', async () => {
    
  502.     let prerendering = true;
    
  503.     function Postpone() {
    
  504.       if (prerendering) {
    
  505.         React.unstable_postpone();
    
  506.       }
    
  507.       return 'World';
    
  508.     }
    
  509. 
    
  510.     function App() {
    
  511.       return (
    
  512.         <div>
    
  513.           <Suspense fallback="Loading...">
    
  514.             Hello
    
  515.             <Postpone />
    
  516.           </Suspense>
    
  517.         </div>
    
  518.       );
    
  519.     }
    
  520. 
    
  521.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  522.     expect(prerendered.postponed).not.toBe(null);
    
  523. 
    
  524.     prerendering = false;
    
  525. 
    
  526.     const resumed = await ReactDOMFizzServer.resume(
    
  527.       <App />,
    
  528.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  529.     );
    
  530. 
    
  531.     await readIntoContainer(prerendered.prelude);
    
  532. 
    
  533.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  534. 
    
  535.     await readIntoContainer(resumed);
    
  536. 
    
  537.     expect(getVisibleChildren(container)).toEqual(
    
  538.       <div>{['Hello', 'World']}</div>,
    
  539.     );
    
  540.   });
    
  541. 
    
  542.   // @gate enablePostpone
    
  543.   it('supports postponing in lazy in prerender and resuming later', async () => {
    
  544.     let prerendering = true;
    
  545.     const Hole = React.lazy(async () => {
    
  546.       React.unstable_postpone();
    
  547.     });
    
  548. 
    
  549.     function App() {
    
  550.       return (
    
  551.         <div>
    
  552.           <Suspense fallback="Loading...">
    
  553.             Hi
    
  554.             {prerendering ? Hole : 'Hello'}
    
  555.           </Suspense>
    
  556.         </div>
    
  557.       );
    
  558.     }
    
  559. 
    
  560.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  561.     expect(prerendered.postponed).not.toBe(null);
    
  562. 
    
  563.     prerendering = false;
    
  564. 
    
  565.     const resumed = await ReactDOMFizzServer.resume(
    
  566.       <App />,
    
  567.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  568.     );
    
  569. 
    
  570.     await readIntoContainer(prerendered.prelude);
    
  571. 
    
  572.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  573. 
    
  574.     await readIntoContainer(resumed);
    
  575. 
    
  576.     expect(getVisibleChildren(container)).toEqual(
    
  577.       <div>
    
  578.         {'Hi'}
    
  579.         {'Hello'}
    
  580.       </div>,
    
  581.     );
    
  582.   });
    
  583. 
    
  584.   // @gate enablePostpone
    
  585.   it('supports postponing in a nested array', async () => {
    
  586.     let prerendering = true;
    
  587.     const Hole = React.lazy(async () => {
    
  588.       React.unstable_postpone();
    
  589.     });
    
  590.     function Postpone() {
    
  591.       if (prerendering) {
    
  592.         React.unstable_postpone();
    
  593.       }
    
  594.       return 'Hello';
    
  595.     }
    
  596. 
    
  597.     function App() {
    
  598.       return (
    
  599.         <div>
    
  600.           <Suspense fallback="Loading...">
    
  601.             Hi
    
  602.             {[<Postpone key="key" />, prerendering ? Hole : 'World']}
    
  603.           </Suspense>
    
  604.         </div>
    
  605.       );
    
  606.     }
    
  607. 
    
  608.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  609.     expect(prerendered.postponed).not.toBe(null);
    
  610. 
    
  611.     prerendering = false;
    
  612. 
    
  613.     const resumed = await ReactDOMFizzServer.resume(
    
  614.       <App />,
    
  615.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  616.     );
    
  617. 
    
  618.     await readIntoContainer(prerendered.prelude);
    
  619. 
    
  620.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  621. 
    
  622.     await readIntoContainer(resumed);
    
  623. 
    
  624.     expect(getVisibleChildren(container)).toEqual(
    
  625.       <div>{['Hi', 'Hello', 'World']}</div>,
    
  626.     );
    
  627.   });
    
  628. 
    
  629.   // @gate enablePostpone
    
  630.   it('supports postponing in lazy as a direct child', async () => {
    
  631.     let prerendering = true;
    
  632.     const Hole = React.lazy(async () => {
    
  633.       React.unstable_postpone();
    
  634.     });
    
  635.     function Postpone() {
    
  636.       return prerendering ? Hole : 'Hello';
    
  637.     }
    
  638. 
    
  639.     function App() {
    
  640.       return (
    
  641.         <div>
    
  642.           <Suspense fallback="Loading...">
    
  643.             <Postpone key="key" />
    
  644.           </Suspense>
    
  645.         </div>
    
  646.       );
    
  647.     }
    
  648. 
    
  649.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  650.     expect(prerendered.postponed).not.toBe(null);
    
  651. 
    
  652.     prerendering = false;
    
  653. 
    
  654.     const resumed = await ReactDOMFizzServer.resume(
    
  655.       <App />,
    
  656.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  657.     );
    
  658. 
    
  659.     await readIntoContainer(prerendered.prelude);
    
  660. 
    
  661.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  662. 
    
  663.     await readIntoContainer(resumed);
    
  664. 
    
  665.     expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
    
  666.   });
    
  667. 
    
  668.   // @gate enablePostpone
    
  669.   it('only emits end tags once when resuming', async () => {
    
  670.     let prerendering = true;
    
  671.     function Postpone() {
    
  672.       if (prerendering) {
    
  673.         React.unstable_postpone();
    
  674.       }
    
  675.       return 'Hello';
    
  676.     }
    
  677. 
    
  678.     function App() {
    
  679.       return (
    
  680.         <html>
    
  681.           <body>
    
  682.             <Suspense fallback="Loading...">
    
  683.               <Postpone />
    
  684.             </Suspense>
    
  685.           </body>
    
  686.         </html>
    
  687.       );
    
  688.     }
    
  689. 
    
  690.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  691.     expect(prerendered.postponed).not.toBe(null);
    
  692. 
    
  693.     prerendering = false;
    
  694. 
    
  695.     const content = await ReactDOMFizzServer.resume(
    
  696.       <App />,
    
  697.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  698.     );
    
  699. 
    
  700.     const html = await readContent(concat(prerendered.prelude, content));
    
  701.     const htmlEndTags = /<\/html\s*>/gi;
    
  702.     const bodyEndTags = /<\/body\s*>/gi;
    
  703.     expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
    
  704.     expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
    
  705.   });
    
  706. 
    
  707.   // @gate enablePostpone
    
  708.   it('can prerender various hoistables and deduped resources', async () => {
    
  709.     let prerendering = true;
    
  710.     function Postpone() {
    
  711.       if (prerendering) {
    
  712.         React.unstable_postpone();
    
  713.       }
    
  714.       return (
    
  715.         <>
    
  716.           <link rel="stylesheet" href="my-style2" precedence="low" />
    
  717.           <link rel="stylesheet" href="my-style1" precedence="high" />
    
  718.           <style precedence="high" href="my-style3">
    
  719.             style
    
  720.           </style>
    
  721.           <img src="my-img" />
    
  722.         </>
    
  723.       );
    
  724.     }
    
  725. 
    
  726.     function App() {
    
  727.       ReactDOM.preconnect('example.com');
    
  728.       ReactDOM.preload('my-font', {as: 'font', type: 'font/woff2'});
    
  729.       ReactDOM.preload('my-style0', {as: 'style'});
    
  730.       // This should transfer the props in to the style that loads later.
    
  731.       ReactDOM.preload('my-style2', {
    
  732.         as: 'style',
    
  733.         crossOrigin: 'use-credentials',
    
  734.       });
    
  735.       return (
    
  736.         <div>
    
  737.           <Suspense fallback="Loading...">
    
  738.             <link rel="stylesheet" href="my-style1" precedence="high" />
    
  739.             <img src="my-img" />
    
  740.             <Postpone />
    
  741.           </Suspense>
    
  742.           <title>Hello World</title>
    
  743.         </div>
    
  744.       );
    
  745.     }
    
  746. 
    
  747.     let calledInit = false;
    
  748.     jest.mock(
    
  749.       'init.js',
    
  750.       () => {
    
  751.         calledInit = true;
    
  752.       },
    
  753.       {virtual: true},
    
  754.     );
    
  755. 
    
  756.     const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
    
  757.       bootstrapScripts: ['init.js'],
    
  758.     });
    
  759.     expect(prerendered.postponed).not.toBe(null);
    
  760. 
    
  761.     await readIntoContainer(prerendered.prelude);
    
  762. 
    
  763.     expect(getVisibleChildren(container)).toEqual([
    
  764.       <link href="example.com" rel="preconnect" />,
    
  765.       <link
    
  766.         as="font"
    
  767.         crossorigin=""
    
  768.         href="my-font"
    
  769.         rel="preload"
    
  770.         type="font/woff2"
    
  771.       />,
    
  772.       <link as="image" href="my-img" rel="preload" />,
    
  773.       <link data-precedence="high" href="my-style1" rel="stylesheet" />,
    
  774.       <link as="script" fetchpriority="low" href="init.js" rel="preload" />,
    
  775.       <link as="style" href="my-style0" rel="preload" />,
    
  776.       <link
    
  777.         as="style"
    
  778.         crossorigin="use-credentials"
    
  779.         href="my-style2"
    
  780.         rel="preload"
    
  781.       />,
    
  782.       <title>Hello World</title>,
    
  783.       <div>Loading...</div>,
    
  784.     ]);
    
  785. 
    
  786.     prerendering = false;
    
  787.     const content = await ReactDOMFizzServer.resume(
    
  788.       <App />,
    
  789.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  790.     );
    
  791. 
    
  792.     await readIntoContainer(content);
    
  793. 
    
  794.     expect(calledInit).toBe(true);
    
  795. 
    
  796.     // Dispatch load event to injected stylesheet
    
  797.     const link = document.querySelector(
    
  798.       'link[rel="stylesheet"][href="my-style2"]',
    
  799.     );
    
  800.     const event = document.createEvent('Events');
    
  801.     event.initEvent('load', true, true);
    
  802.     link.dispatchEvent(event);
    
  803. 
    
  804.     // Wait for the instruction microtasks to flush.
    
  805.     await 0;
    
  806.     await 0;
    
  807. 
    
  808.     expect(getVisibleChildren(container)).toEqual([
    
  809.       <link href="example.com" rel="preconnect" />,
    
  810.       <link
    
  811.         as="font"
    
  812.         crossorigin=""
    
  813.         href="my-font"
    
  814.         rel="preload"
    
  815.         type="font/woff2"
    
  816.       />,
    
  817.       <link as="image" href="my-img" rel="preload" />,
    
  818.       <link data-precedence="high" href="my-style1" rel="stylesheet" />,
    
  819.       <style data-href="my-style3" data-precedence="high">
    
  820.         style
    
  821.       </style>,
    
  822.       <link
    
  823.         crossorigin="use-credentials"
    
  824.         data-precedence="low"
    
  825.         href="my-style2"
    
  826.         rel="stylesheet"
    
  827.       />,
    
  828.       <link as="script" fetchpriority="low" href="init.js" rel="preload" />,
    
  829.       <link as="style" href="my-style0" rel="preload" />,
    
  830.       <link
    
  831.         as="style"
    
  832.         crossorigin="use-credentials"
    
  833.         href="my-style2"
    
  834.         rel="preload"
    
  835.       />,
    
  836.       <title>Hello World</title>,
    
  837.       <div>
    
  838.         <img src="my-img" />
    
  839.         <img src="my-img" />
    
  840.       </div>,
    
  841.     ]);
    
  842.   });
    
  843. 
    
  844.   // @gate enablePostpone
    
  845.   it('can postpone a boundary after it has already been added', async () => {
    
  846.     let prerendering = true;
    
  847.     function Postpone() {
    
  848.       if (prerendering) {
    
  849.         React.unstable_postpone();
    
  850.       }
    
  851.       return 'Hello';
    
  852.     }
    
  853. 
    
  854.     function App() {
    
  855.       return (
    
  856.         <div>
    
  857.           <Suspense fallback="Loading...">
    
  858.             <Suspense fallback="Loading...">
    
  859.               <Postpone />
    
  860.             </Suspense>
    
  861.             <Postpone />
    
  862.             <Postpone />
    
  863.           </Suspense>
    
  864.         </div>
    
  865.       );
    
  866.     }
    
  867. 
    
  868.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  869.     expect(prerendered.postponed).not.toBe(null);
    
  870. 
    
  871.     prerendering = false;
    
  872. 
    
  873.     const resumed = await ReactDOMFizzServer.resume(
    
  874.       <App />,
    
  875.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  876.     );
    
  877. 
    
  878.     await readIntoContainer(prerendered.prelude);
    
  879. 
    
  880.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  881. 
    
  882.     await readIntoContainer(resumed);
    
  883. 
    
  884.     expect(getVisibleChildren(container)).toEqual(
    
  885.       <div>{['Hello', 'Hello', 'Hello']}</div>,
    
  886.     );
    
  887.   });
    
  888. 
    
  889.   // @gate enablePostpone
    
  890.   it('can postpone in fallback', async () => {
    
  891.     let prerendering = true;
    
  892.     function Postpone() {
    
  893.       if (prerendering) {
    
  894.         React.unstable_postpone();
    
  895.       }
    
  896.       return 'Hello';
    
  897.     }
    
  898. 
    
  899.     const Lazy = React.lazy(async () => {
    
  900.       await 0;
    
  901.       return {default: Postpone};
    
  902.     });
    
  903. 
    
  904.     function App() {
    
  905.       return (
    
  906.         <div>
    
  907.           <Suspense fallback="Outer">
    
  908.             <Suspense fallback={<Postpone />}>
    
  909.               <Postpone /> World
    
  910.             </Suspense>
    
  911.             <Suspense fallback={<Postpone />}>
    
  912.               <Lazy />
    
  913.             </Suspense>
    
  914.           </Suspense>
    
  915.         </div>
    
  916.       );
    
  917.     }
    
  918. 
    
  919.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  920.     expect(prerendered.postponed).not.toBe(null);
    
  921. 
    
  922.     prerendering = false;
    
  923. 
    
  924.     const resumed = await ReactDOMFizzServer.resume(
    
  925.       <App />,
    
  926.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  927.     );
    
  928. 
    
  929.     await readIntoContainer(prerendered.prelude);
    
  930. 
    
  931.     expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
    
  932. 
    
  933.     await readIntoContainer(resumed);
    
  934. 
    
  935.     expect(getVisibleChildren(container)).toEqual(
    
  936.       <div>
    
  937.         {'Hello'}
    
  938.         {' World'}
    
  939.         {'Hello'}
    
  940.       </div>,
    
  941.     );
    
  942.   });
    
  943. 
    
  944.   // @gate enablePostpone
    
  945.   it('can postpone in fallback without postponing the tree', async () => {
    
  946.     function Postpone() {
    
  947.       React.unstable_postpone();
    
  948.     }
    
  949. 
    
  950.     const lazyText = React.lazy(async () => {
    
  951.       await 0; // causes the fallback to start work
    
  952.       return {default: 'Hello'};
    
  953.     });
    
  954. 
    
  955.     function App() {
    
  956.       return (
    
  957.         <div>
    
  958.           <Suspense fallback="Outer">
    
  959.             <Suspense fallback={<Postpone />}>{lazyText}</Suspense>
    
  960.           </Suspense>
    
  961.         </div>
    
  962.       );
    
  963.     }
    
  964. 
    
  965.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  966.     // TODO: This should actually be null because we should've been able to fully
    
  967.     // resolve the render on the server eventually, even though the fallback postponed.
    
  968.     // So we should not need to resume.
    
  969.     expect(prerendered.postponed).not.toBe(null);
    
  970. 
    
  971.     await readIntoContainer(prerendered.prelude);
    
  972. 
    
  973.     expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
    
  974. 
    
  975.     const resumed = await ReactDOMFizzServer.resume(
    
  976.       <App />,
    
  977.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  978.     );
    
  979. 
    
  980.     await readIntoContainer(resumed);
    
  981. 
    
  982.     expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
    
  983.   });
    
  984. 
    
  985.   // @gate enablePostpone
    
  986.   it('errors if the replay does not line up', async () => {
    
  987.     let prerendering = true;
    
  988.     function Postpone() {
    
  989.       if (prerendering) {
    
  990.         React.unstable_postpone();
    
  991.       }
    
  992.       return 'Hello';
    
  993.     }
    
  994. 
    
  995.     function Wrapper({children}) {
    
  996.       return children;
    
  997.     }
    
  998. 
    
  999.     const lazySpan = React.lazy(async () => {
    
  1000.       await 0;
    
  1001.       return {default: <span />};
    
  1002.     });
    
  1003. 
    
  1004.     function App() {
    
  1005.       const children = (
    
  1006.         <Suspense fallback="Loading...">
    
  1007.           <Postpone />
    
  1008.         </Suspense>
    
  1009.       );
    
  1010.       return (
    
  1011.         <>
    
  1012.           <div>{prerendering ? <Wrapper>{children}</Wrapper> : children}</div>
    
  1013.           <div>
    
  1014.             {prerendering ? (
    
  1015.               <Suspense fallback="Loading...">
    
  1016.                 <div>
    
  1017.                   <Postpone />
    
  1018.                 </div>
    
  1019.               </Suspense>
    
  1020.             ) : (
    
  1021.               lazySpan
    
  1022.             )}
    
  1023.           </div>
    
  1024.         </>
    
  1025.       );
    
  1026.     }
    
  1027. 
    
  1028.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1029.     expect(prerendered.postponed).not.toBe(null);
    
  1030. 
    
  1031.     await readIntoContainer(prerendered.prelude);
    
  1032. 
    
  1033.     expect(getVisibleChildren(container)).toEqual([
    
  1034.       <div>Loading...</div>,
    
  1035.       <div>Loading...</div>,
    
  1036.     ]);
    
  1037. 
    
  1038.     prerendering = false;
    
  1039. 
    
  1040.     const errors = [];
    
  1041.     const resumed = await ReactDOMFizzServer.resume(
    
  1042.       <App />,
    
  1043.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1044.       {
    
  1045.         onError(x) {
    
  1046.           errors.push(x.message);
    
  1047.         },
    
  1048.       },
    
  1049.     );
    
  1050. 
    
  1051.     expect(errors).toEqual([
    
  1052.       'Expected the resume to render <Wrapper> in this slot but instead it rendered <Suspense>. ' +
    
  1053.         "The tree doesn't match so React will fallback to client rendering.",
    
  1054.       'Expected the resume to render <Suspense> in this slot but instead it rendered <span>. ' +
    
  1055.         "The tree doesn't match so React will fallback to client rendering.",
    
  1056.     ]);
    
  1057. 
    
  1058.     // TODO: Test the component stack but we don't expose it to the server yet.
    
  1059. 
    
  1060.     await readIntoContainer(resumed);
    
  1061. 
    
  1062.     // Client rendered
    
  1063.     expect(getVisibleChildren(container)).toEqual([
    
  1064.       <div>Loading...</div>,
    
  1065.       <div>Loading...</div>,
    
  1066.     ]);
    
  1067.   });
    
  1068. 
    
  1069.   // @gate enablePostpone
    
  1070.   it('can abort the resume', async () => {
    
  1071.     let prerendering = true;
    
  1072.     const infinitePromise = new Promise(() => {});
    
  1073.     function Postpone() {
    
  1074.       if (prerendering) {
    
  1075.         React.unstable_postpone();
    
  1076.       }
    
  1077.       return 'Hello';
    
  1078.     }
    
  1079. 
    
  1080.     function App() {
    
  1081.       if (!prerendering) {
    
  1082.         React.use(infinitePromise);
    
  1083.       }
    
  1084.       return (
    
  1085.         <div>
    
  1086.           <Suspense fallback="Loading...">
    
  1087.             <Postpone />
    
  1088.           </Suspense>
    
  1089.         </div>
    
  1090.       );
    
  1091.     }
    
  1092. 
    
  1093.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1094.     expect(prerendered.postponed).not.toBe(null);
    
  1095. 
    
  1096.     await readIntoContainer(prerendered.prelude);
    
  1097. 
    
  1098.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  1099. 
    
  1100.     prerendering = false;
    
  1101. 
    
  1102.     const controller = new AbortController();
    
  1103. 
    
  1104.     const errors = [];
    
  1105. 
    
  1106.     const resumedPromise = ReactDOMFizzServer.resume(
    
  1107.       <App />,
    
  1108.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1109.       {
    
  1110.         signal: controller.signal,
    
  1111.         onError(x) {
    
  1112.           errors.push(x);
    
  1113.         },
    
  1114.       },
    
  1115.     );
    
  1116. 
    
  1117.     controller.abort('abort');
    
  1118. 
    
  1119.     const resumed = await resumedPromise;
    
  1120.     await resumed.allReady;
    
  1121. 
    
  1122.     expect(errors).toEqual(['abort']);
    
  1123. 
    
  1124.     await readIntoContainer(resumed);
    
  1125. 
    
  1126.     // Client rendered
    
  1127.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  1128.   });
    
  1129. 
    
  1130.   // @gate enablePostpone
    
  1131.   it('can suspend in a replayed component several layers deep', async () => {
    
  1132.     let prerendering = true;
    
  1133.     function Postpone() {
    
  1134.       if (prerendering) {
    
  1135.         React.unstable_postpone();
    
  1136.       }
    
  1137.       return 'Hello';
    
  1138.     }
    
  1139. 
    
  1140.     let resolve;
    
  1141.     const promise = new Promise(r => (resolve = r));
    
  1142.     function Delay({children}) {
    
  1143.       if (!prerendering) {
    
  1144.         React.use(promise);
    
  1145.       }
    
  1146.       return children;
    
  1147.     }
    
  1148. 
    
  1149.     // This wrapper will cause us to do one destructive render past this.
    
  1150.     function Outer({children}) {
    
  1151.       return children;
    
  1152.     }
    
  1153. 
    
  1154.     function App() {
    
  1155.       return (
    
  1156.         <div>
    
  1157.           <Outer>
    
  1158.             <Delay>
    
  1159.               <Suspense fallback="Loading...">
    
  1160.                 <Postpone />
    
  1161.               </Suspense>
    
  1162.             </Delay>
    
  1163.           </Outer>
    
  1164.         </div>
    
  1165.       );
    
  1166.     }
    
  1167. 
    
  1168.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1169.     expect(prerendered.postponed).not.toBe(null);
    
  1170. 
    
  1171.     await readIntoContainer(prerendered.prelude);
    
  1172. 
    
  1173.     prerendering = false;
    
  1174. 
    
  1175.     const resumedPromise = ReactDOMFizzServer.resume(
    
  1176.       <App />,
    
  1177.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1178.     );
    
  1179. 
    
  1180.     await jest.runAllTimers();
    
  1181. 
    
  1182.     expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
    
  1183. 
    
  1184.     await resolve();
    
  1185. 
    
  1186.     await readIntoContainer(await resumedPromise);
    
  1187. 
    
  1188.     expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
    
  1189.   });
    
  1190. 
    
  1191.   // @gate enablePostpone
    
  1192.   it('emits an empty prelude and resumes at the root if we postpone in the shell', async () => {
    
  1193.     let prerendering = true;
    
  1194.     function Postpone() {
    
  1195.       if (prerendering) {
    
  1196.         React.unstable_postpone();
    
  1197.       }
    
  1198.       return 'Hello';
    
  1199.     }
    
  1200. 
    
  1201.     function App() {
    
  1202.       return (
    
  1203.         <html lang="en">
    
  1204.           <body>
    
  1205.             <link rel="stylesheet" href="my-style" precedence="high" />
    
  1206.             <Postpone />
    
  1207.           </body>
    
  1208.         </html>
    
  1209.       );
    
  1210.     }
    
  1211. 
    
  1212.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1213.     expect(prerendered.postponed).not.toBe(null);
    
  1214. 
    
  1215.     prerendering = false;
    
  1216. 
    
  1217.     expect(await readContent(prerendered.prelude)).toBe('');
    
  1218. 
    
  1219.     const content = await ReactDOMFizzServer.resume(
    
  1220.       <App />,
    
  1221.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1222.     );
    
  1223. 
    
  1224.     expect(await readContent(content)).toBe(
    
  1225.       '<!DOCTYPE html><html lang="en"><head>' +
    
  1226.         '<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
    
  1227.         '</head><body>Hello</body></html>',
    
  1228.     );
    
  1229.   });
    
  1230. 
    
  1231.   // @gate enablePostpone
    
  1232.   it('emits an empty prelude if we have not rendered html or head tags yet', async () => {
    
  1233.     let prerendering = true;
    
  1234.     function Postpone() {
    
  1235.       if (prerendering) {
    
  1236.         React.unstable_postpone();
    
  1237.       }
    
  1238.       return (
    
  1239.         <html lang="en">
    
  1240.           <body>Hello</body>
    
  1241.         </html>
    
  1242.       );
    
  1243.     }
    
  1244. 
    
  1245.     function App() {
    
  1246.       return (
    
  1247.         <>
    
  1248.           <link rel="stylesheet" href="my-style" precedence="high" />
    
  1249.           <Postpone />
    
  1250.         </>
    
  1251.       );
    
  1252.     }
    
  1253. 
    
  1254.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1255.     expect(prerendered.postponed).not.toBe(null);
    
  1256. 
    
  1257.     prerendering = false;
    
  1258. 
    
  1259.     expect(await readContent(prerendered.prelude)).toBe('');
    
  1260. 
    
  1261.     const content = await ReactDOMFizzServer.resume(
    
  1262.       <App />,
    
  1263.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1264.     );
    
  1265. 
    
  1266.     expect(await readContent(content)).toBe(
    
  1267.       '<!DOCTYPE html><html lang="en"><head>' +
    
  1268.         '<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
    
  1269.         '</head><body>Hello</body></html>',
    
  1270.     );
    
  1271.   });
    
  1272. 
    
  1273.   // @gate enablePostpone
    
  1274.   it('emits an empty prelude if a postpone in a promise in the shell', async () => {
    
  1275.     let prerendering = true;
    
  1276.     function Postpone() {
    
  1277.       if (prerendering) {
    
  1278.         React.unstable_postpone();
    
  1279.       }
    
  1280.       return 'Hello';
    
  1281.     }
    
  1282. 
    
  1283.     const Lazy = React.lazy(async () => {
    
  1284.       await 0;
    
  1285.       return {default: Postpone};
    
  1286.     });
    
  1287. 
    
  1288.     function App() {
    
  1289.       return (
    
  1290.         <html>
    
  1291.           <link rel="stylesheet" href="my-style" precedence="high" />
    
  1292.           <body>
    
  1293.             <div>
    
  1294.               <Lazy />
    
  1295.             </div>
    
  1296.           </body>
    
  1297.         </html>
    
  1298.       );
    
  1299.     }
    
  1300. 
    
  1301.     const prerendered = await ReactDOMFizzStatic.prerender(<App />);
    
  1302.     expect(prerendered.postponed).not.toBe(null);
    
  1303. 
    
  1304.     prerendering = false;
    
  1305. 
    
  1306.     expect(await readContent(prerendered.prelude)).toBe('');
    
  1307. 
    
  1308.     const content = await ReactDOMFizzServer.resume(
    
  1309.       <App />,
    
  1310.       JSON.parse(JSON.stringify(prerendered.postponed)),
    
  1311.     );
    
  1312. 
    
  1313.     expect(await readContent(content)).toBe(
    
  1314.       '<!DOCTYPE html><html><head>' +
    
  1315.         '<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
    
  1316.         '</head><body><div>Hello</div></body></html>',
    
  1317.     );
    
  1318.   });
    
  1319. });