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. let React;
    
  13. let ReactDOM;
    
  14. let Suspense;
    
  15. let Scheduler;
    
  16. let act;
    
  17. let textCache;
    
  18. 
    
  19. describe('ReactDOMSuspensePlaceholder', () => {
    
  20.   let container;
    
  21. 
    
  22.   beforeEach(() => {
    
  23.     jest.resetModules();
    
  24.     React = require('react');
    
  25.     ReactDOM = require('react-dom');
    
  26.     Scheduler = require('scheduler');
    
  27.     act = require('internal-test-utils').act;
    
  28.     Suspense = React.Suspense;
    
  29.     container = document.createElement('div');
    
  30.     document.body.appendChild(container);
    
  31. 
    
  32.     textCache = new Map();
    
  33.   });
    
  34. 
    
  35.   afterEach(() => {
    
  36.     document.body.removeChild(container);
    
  37.   });
    
  38. 
    
  39.   function resolveText(text) {
    
  40.     const record = textCache.get(text);
    
  41.     if (record === undefined) {
    
  42.       const newRecord = {
    
  43.         status: 'resolved',
    
  44.         value: text,
    
  45.       };
    
  46.       textCache.set(text, newRecord);
    
  47.     } else if (record.status === 'pending') {
    
  48.       const thenable = record.value;
    
  49.       record.status = 'resolved';
    
  50.       record.value = text;
    
  51.       thenable.pings.forEach(t => t());
    
  52.     }
    
  53.   }
    
  54. 
    
  55.   function readText(text) {
    
  56.     const record = textCache.get(text);
    
  57.     if (record !== undefined) {
    
  58.       switch (record.status) {
    
  59.         case 'pending':
    
  60.           Scheduler.log(`Suspend! [${text}]`);
    
  61.           throw record.value;
    
  62.         case 'rejected':
    
  63.           throw record.value;
    
  64.         case 'resolved':
    
  65.           return record.value;
    
  66.       }
    
  67.     } else {
    
  68.       Scheduler.log(`Suspend! [${text}]`);
    
  69.       const thenable = {
    
  70.         pings: [],
    
  71.         then(resolve) {
    
  72.           if (newRecord.status === 'pending') {
    
  73.             thenable.pings.push(resolve);
    
  74.           } else {
    
  75.             Promise.resolve().then(() => resolve(newRecord.value));
    
  76.           }
    
  77.         },
    
  78.       };
    
  79. 
    
  80.       const newRecord = {
    
  81.         status: 'pending',
    
  82.         value: thenable,
    
  83.       };
    
  84.       textCache.set(text, newRecord);
    
  85. 
    
  86.       throw thenable;
    
  87.     }
    
  88.   }
    
  89. 
    
  90.   function Text({text}) {
    
  91.     Scheduler.log(text);
    
  92.     return text;
    
  93.   }
    
  94. 
    
  95.   function AsyncText({text}) {
    
  96.     readText(text);
    
  97.     Scheduler.log(text);
    
  98.     return text;
    
  99.   }
    
  100. 
    
  101.   it('hides and unhides timed out DOM elements', async () => {
    
  102.     const divs = [
    
  103.       React.createRef(null),
    
  104.       React.createRef(null),
    
  105.       React.createRef(null),
    
  106.     ];
    
  107.     function App() {
    
  108.       return (
    
  109.         <Suspense fallback={<Text text="Loading..." />}>
    
  110.           <div ref={divs[0]}>
    
  111.             <Text text="A" />
    
  112.           </div>
    
  113.           <div ref={divs[1]}>
    
  114.             <AsyncText text="B" />
    
  115.           </div>
    
  116.           <div style={{display: 'inline'}} ref={divs[2]}>
    
  117.             <Text text="C" />
    
  118.           </div>
    
  119.         </Suspense>
    
  120.       );
    
  121.     }
    
  122.     ReactDOM.render(<App />, container);
    
  123.     expect(window.getComputedStyle(divs[0].current).display).toEqual('none');
    
  124.     expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
    
  125.     expect(window.getComputedStyle(divs[2].current).display).toEqual('none');
    
  126. 
    
  127.     await act(async () => {
    
  128.       await resolveText('B');
    
  129.     });
    
  130. 
    
  131.     expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
    
  132.     expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
    
  133.     // This div's display was set with a prop.
    
  134.     expect(window.getComputedStyle(divs[2].current).display).toEqual('inline');
    
  135.   });
    
  136. 
    
  137.   it('hides and unhides timed out text nodes', async () => {
    
  138.     function App() {
    
  139.       return (
    
  140.         <Suspense fallback={<Text text="Loading..." />}>
    
  141.           <Text text="A" />
    
  142.           <AsyncText text="B" />
    
  143.           <Text text="C" />
    
  144.         </Suspense>
    
  145.       );
    
  146.     }
    
  147.     ReactDOM.render(<App />, container);
    
  148.     expect(container.textContent).toEqual('Loading...');
    
  149. 
    
  150.     await act(async () => {
    
  151.       await resolveText('B');
    
  152.     });
    
  153. 
    
  154.     expect(container.textContent).toEqual('ABC');
    
  155.   });
    
  156. 
    
  157.   it(
    
  158.     'outside concurrent mode, re-hides children if their display is updated ' +
    
  159.       'but the boundary is still showing the fallback',
    
  160.     async () => {
    
  161.       const {useState} = React;
    
  162. 
    
  163.       let setIsVisible;
    
  164.       function Sibling({children}) {
    
  165.         const [isVisible, _setIsVisible] = useState(false);
    
  166.         setIsVisible = _setIsVisible;
    
  167.         return (
    
  168.           <span style={{display: isVisible ? 'inline' : 'none'}}>
    
  169.             {children}
    
  170.           </span>
    
  171.         );
    
  172.       }
    
  173. 
    
  174.       function App() {
    
  175.         return (
    
  176.           <Suspense fallback={<Text text="Loading..." />}>
    
  177.             <Sibling>Sibling</Sibling>
    
  178.             <span>
    
  179.               <AsyncText text="Async" />
    
  180.             </span>
    
  181.           </Suspense>
    
  182.         );
    
  183.       }
    
  184. 
    
  185.       await act(() => {
    
  186.         ReactDOM.render(<App />, container);
    
  187.       });
    
  188.       expect(container.innerHTML).toEqual(
    
  189.         '<span style="display: none;">Sibling</span><span style=' +
    
  190.           '"display: none;"></span>Loading...',
    
  191.       );
    
  192. 
    
  193.       // Update the inline display style. It will be overridden because it's
    
  194.       // inside a hidden fallback.
    
  195.       await act(() => setIsVisible(true));
    
  196.       expect(container.innerHTML).toEqual(
    
  197.         '<span style="display: none;">Sibling</span><span style=' +
    
  198.           '"display: none;"></span>Loading...',
    
  199.       );
    
  200. 
    
  201.       // Unsuspend. The style should now match the inline prop.
    
  202.       await act(() => resolveText('Async'));
    
  203.       expect(container.innerHTML).toEqual(
    
  204.         '<span style="display: inline;">Sibling</span><span style="">Async</span>',
    
  205.       );
    
  206.     },
    
  207.   );
    
  208. 
    
  209.   // Regression test for https://github.com/facebook/react/issues/14188
    
  210.   it('can call findDOMNode() in a suspended component commit phase', async () => {
    
  211.     const log = [];
    
  212.     const Lazy = React.lazy(
    
  213.       () =>
    
  214.         new Promise(resolve =>
    
  215.           resolve({
    
  216.             default() {
    
  217.               return 'lazy';
    
  218.             },
    
  219.           }),
    
  220.         ),
    
  221.     );
    
  222. 
    
  223.     class Child extends React.Component {
    
  224.       componentDidMount() {
    
  225.         log.push('cDM ' + this.props.id);
    
  226.         ReactDOM.findDOMNode(this);
    
  227.       }
    
  228.       componentDidUpdate() {
    
  229.         log.push('cDU ' + this.props.id);
    
  230.         ReactDOM.findDOMNode(this);
    
  231.       }
    
  232.       render() {
    
  233.         return 'child';
    
  234.       }
    
  235.     }
    
  236. 
    
  237.     const buttonRef = React.createRef();
    
  238.     class App extends React.Component {
    
  239.       state = {
    
  240.         suspend: false,
    
  241.       };
    
  242.       handleClick = () => {
    
  243.         this.setState({suspend: true});
    
  244.       };
    
  245.       render() {
    
  246.         return (
    
  247.           <React.Suspense fallback="Loading">
    
  248.             <Child id="first" />
    
  249.             <button ref={buttonRef} onClick={this.handleClick}>
    
  250.               Suspend
    
  251.             </button>
    
  252.             <Child id="second" />
    
  253.             {this.state.suspend && <Lazy />}
    
  254.           </React.Suspense>
    
  255.         );
    
  256.       }
    
  257.     }
    
  258. 
    
  259.     ReactDOM.render(<App />, container);
    
  260. 
    
  261.     expect(log).toEqual(['cDM first', 'cDM second']);
    
  262.     log.length = 0;
    
  263. 
    
  264.     buttonRef.current.dispatchEvent(new MouseEvent('click', {bubbles: true}));
    
  265.     await Lazy;
    
  266.     expect(log).toEqual(['cDU first', 'cDU second']);
    
  267.   });
    
  268. 
    
  269.   // Regression test for https://github.com/facebook/react/issues/14188
    
  270.   it('can call findDOMNode() in a suspended component commit phase (#2)', () => {
    
  271.     let suspendOnce = Promise.resolve();
    
  272.     function Suspend() {
    
  273.       if (suspendOnce) {
    
  274.         const promise = suspendOnce;
    
  275.         suspendOnce = null;
    
  276.         throw promise;
    
  277.       }
    
  278.       return null;
    
  279.     }
    
  280. 
    
  281.     const log = [];
    
  282.     class Child extends React.Component {
    
  283.       componentDidMount() {
    
  284.         log.push('cDM');
    
  285.         ReactDOM.findDOMNode(this);
    
  286.       }
    
  287. 
    
  288.       componentDidUpdate() {
    
  289.         log.push('cDU');
    
  290.         ReactDOM.findDOMNode(this);
    
  291.       }
    
  292. 
    
  293.       render() {
    
  294.         return null;
    
  295.       }
    
  296.     }
    
  297. 
    
  298.     function App() {
    
  299.       return (
    
  300.         <Suspense fallback="Loading">
    
  301.           <Suspend />
    
  302.           <Child />
    
  303.         </Suspense>
    
  304.       );
    
  305.     }
    
  306. 
    
  307.     ReactDOM.render(<App />, container);
    
  308.     expect(log).toEqual(['cDM']);
    
  309.     ReactDOM.render(<App />, container);
    
  310.     expect(log).toEqual(['cDM', 'cDU']);
    
  311.   });
    
  312. });