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. const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
    
  13. 
    
  14. let React;
    
  15. let ReactDOM;
    
  16. let ReactDOMClient;
    
  17. let ReactDOMServer;
    
  18. let ReactTestUtils;
    
  19. let act;
    
  20. let SuspenseList;
    
  21. 
    
  22. function initModules() {
    
  23.   // Reset warning cache.
    
  24.   jest.resetModules();
    
  25. 
    
  26.   React = require('react');
    
  27.   ReactDOM = require('react-dom');
    
  28.   ReactDOMClient = require('react-dom/client');
    
  29.   ReactDOMServer = require('react-dom/server');
    
  30.   ReactTestUtils = require('react-dom/test-utils');
    
  31.   act = require('internal-test-utils').act;
    
  32.   if (gate(flags => flags.enableSuspenseList)) {
    
  33.     SuspenseList = React.unstable_SuspenseList;
    
  34.   }
    
  35. 
    
  36.   // Make them available to the helpers.
    
  37.   return {
    
  38.     ReactDOM,
    
  39.     ReactDOMServer,
    
  40.     ReactTestUtils,
    
  41.   };
    
  42. }
    
  43. 
    
  44. const {itThrowsWhenRendering, resetModules, serverRender} =
    
  45.   ReactDOMServerIntegrationUtils(initModules);
    
  46. 
    
  47. describe('ReactDOMServerSuspense', () => {
    
  48.   beforeEach(() => {
    
  49.     resetModules();
    
  50.   });
    
  51. 
    
  52.   function Text(props) {
    
  53.     return <div>{props.text}</div>;
    
  54.   }
    
  55. 
    
  56.   function AsyncText(props) {
    
  57.     throw new Promise(() => {});
    
  58.   }
    
  59. 
    
  60.   function getVisibleChildren(element) {
    
  61.     const children = [];
    
  62.     let node = element.firstChild;
    
  63.     while (node) {
    
  64.       if (node.nodeType === 1) {
    
  65.         if (
    
  66.           node.tagName !== 'SCRIPT' &&
    
  67.           node.tagName !== 'TEMPLATE' &&
    
  68.           node.tagName !== 'template' &&
    
  69.           !node.hasAttribute('hidden') &&
    
  70.           !node.hasAttribute('aria-hidden')
    
  71.         ) {
    
  72.           const props = {};
    
  73.           const attributes = node.attributes;
    
  74.           for (let i = 0; i < attributes.length; i++) {
    
  75.             if (
    
  76.               attributes[i].name === 'id' &&
    
  77.               attributes[i].value.includes(':')
    
  78.             ) {
    
  79.               // We assume this is a React added ID that's a non-visual implementation detail.
    
  80.               continue;
    
  81.             }
    
  82.             props[attributes[i].name] = attributes[i].value;
    
  83.           }
    
  84.           props.children = getVisibleChildren(node);
    
  85.           children.push(React.createElement(node.tagName.toLowerCase(), props));
    
  86.         }
    
  87.       } else if (node.nodeType === 3) {
    
  88.         children.push(node.data);
    
  89.       }
    
  90.       node = node.nextSibling;
    
  91.     }
    
  92.     return children.length === 0
    
  93.       ? undefined
    
  94.       : children.length === 1
    
  95.       ? children[0]
    
  96.       : children;
    
  97.   }
    
  98. 
    
  99.   it('should render the children when no promise is thrown', async () => {
    
  100.     const c = await serverRender(
    
  101.       <div>
    
  102.         <React.Suspense fallback={<Text text="Fallback" />}>
    
  103.           <Text text="Children" />
    
  104.         </React.Suspense>
    
  105.       </div>,
    
  106.     );
    
  107.     expect(getVisibleChildren(c)).toEqual(<div>Children</div>);
    
  108.   });
    
  109. 
    
  110.   it('should render the fallback when a promise thrown', async () => {
    
  111.     const c = await serverRender(
    
  112.       <div>
    
  113.         <React.Suspense fallback={<Text text="Fallback" />}>
    
  114.           <AsyncText text="Children" />
    
  115.         </React.Suspense>
    
  116.       </div>,
    
  117.     );
    
  118.     expect(getVisibleChildren(c)).toEqual(<div>Fallback</div>);
    
  119.   });
    
  120. 
    
  121.   it('should work with nested suspense components', async () => {
    
  122.     const c = await serverRender(
    
  123.       <div>
    
  124.         <React.Suspense fallback={<Text text="Fallback" />}>
    
  125.           <div>
    
  126.             <Text text="Children" />
    
  127.             <React.Suspense fallback={<Text text="Fallback" />}>
    
  128.               <AsyncText text="Children" />
    
  129.             </React.Suspense>
    
  130.           </div>
    
  131.         </React.Suspense>
    
  132.       </div>,
    
  133.     );
    
  134. 
    
  135.     expect(getVisibleChildren(c)).toEqual(
    
  136.       <div>
    
  137.         <div>Children</div>
    
  138.         <div>Fallback</div>
    
  139.       </div>,
    
  140.     );
    
  141.   });
    
  142. 
    
  143.   // @gate enableSuspenseList
    
  144.   it('server renders a SuspenseList component and its children', async () => {
    
  145.     const example = (
    
  146.       <SuspenseList>
    
  147.         <React.Suspense fallback="Loading A">
    
  148.           <div>A</div>
    
  149.         </React.Suspense>
    
  150.         <React.Suspense fallback="Loading B">
    
  151.           <div>B</div>
    
  152.         </React.Suspense>
    
  153.       </SuspenseList>
    
  154.     );
    
  155.     const element = await serverRender(example);
    
  156.     const parent = element.parentNode;
    
  157.     const divA = parent.children[0];
    
  158.     expect(divA.tagName).toBe('DIV');
    
  159.     expect(divA.textContent).toBe('A');
    
  160.     const divB = parent.children[1];
    
  161.     expect(divB.tagName).toBe('DIV');
    
  162.     expect(divB.textContent).toBe('B');
    
  163. 
    
  164.     await act(() => {
    
  165.       ReactDOMClient.hydrateRoot(parent, example);
    
  166.     });
    
  167. 
    
  168.     const parent2 = element.parentNode;
    
  169.     const divA2 = parent2.children[0];
    
  170.     const divB2 = parent2.children[1];
    
  171.     expect(divA).toBe(divA2);
    
  172.     expect(divB).toBe(divB2);
    
  173.   });
    
  174. 
    
  175.   // TODO: Remove this in favor of @gate pragma
    
  176.   if (__EXPERIMENTAL__) {
    
  177.     itThrowsWhenRendering(
    
  178.       'a suspending component outside a Suspense node',
    
  179.       async render => {
    
  180.         await render(
    
  181.           <div>
    
  182.             <React.Suspense />
    
  183.             <AsyncText text="Children" />
    
  184.             <React.Suspense />
    
  185.           </div>,
    
  186.           1,
    
  187.         );
    
  188.       },
    
  189.       'A component suspended while responding to synchronous input.',
    
  190.     );
    
  191. 
    
  192.     itThrowsWhenRendering(
    
  193.       'a suspending component without a Suspense above',
    
  194.       async render => {
    
  195.         await render(
    
  196.           <div>
    
  197.             <AsyncText text="Children" />
    
  198.           </div>,
    
  199.           1,
    
  200.         );
    
  201.       },
    
  202.       'A component suspended while responding to synchronous input.',
    
  203.     );
    
  204.   }
    
  205. 
    
  206.   it('does not get confused by throwing null', () => {
    
  207.     function Bad() {
    
  208.       // eslint-disable-next-line no-throw-literal
    
  209.       throw null;
    
  210.     }
    
  211. 
    
  212.     let didError;
    
  213.     let error;
    
  214.     try {
    
  215.       ReactDOMServer.renderToString(<Bad />);
    
  216.     } catch (err) {
    
  217.       didError = true;
    
  218.       error = err;
    
  219.     }
    
  220.     expect(didError).toBe(true);
    
  221.     expect(error).toBe(null);
    
  222.   });
    
  223. 
    
  224.   it('does not get confused by throwing undefined', () => {
    
  225.     function Bad() {
    
  226.       // eslint-disable-next-line no-throw-literal
    
  227.       throw undefined;
    
  228.     }
    
  229. 
    
  230.     let didError;
    
  231.     let error;
    
  232.     try {
    
  233.       ReactDOMServer.renderToString(<Bad />);
    
  234.     } catch (err) {
    
  235.       didError = true;
    
  236.       error = err;
    
  237.     }
    
  238.     expect(didError).toBe(true);
    
  239.     expect(error).toBe(undefined);
    
  240.   });
    
  241. 
    
  242.   it('does not get confused by throwing a primitive', () => {
    
  243.     function Bad() {
    
  244.       // eslint-disable-next-line no-throw-literal
    
  245.       throw 'foo';
    
  246.     }
    
  247. 
    
  248.     let didError;
    
  249.     let error;
    
  250.     try {
    
  251.       ReactDOMServer.renderToString(<Bad />);
    
  252.     } catch (err) {
    
  253.       didError = true;
    
  254.       error = err;
    
  255.     }
    
  256.     expect(didError).toBe(true);
    
  257.     expect(error).toBe('foo');
    
  258.   });
    
  259. });