1. let React;
    
  2. let ReactNoop;
    
  3. let Scheduler;
    
  4. let act;
    
  5. let LegacyHidden;
    
  6. let Activity;
    
  7. let Suspense;
    
  8. let useState;
    
  9. let useEffect;
    
  10. let startTransition;
    
  11. let textCache;
    
  12. let waitFor;
    
  13. let waitForPaint;
    
  14. let assertLog;
    
  15. 
    
  16. describe('Activity Suspense', () => {
    
  17.   beforeEach(() => {
    
  18.     jest.resetModules();
    
  19. 
    
  20.     React = require('react');
    
  21.     ReactNoop = require('react-noop-renderer');
    
  22.     Scheduler = require('scheduler');
    
  23.     act = require('internal-test-utils').act;
    
  24.     LegacyHidden = React.unstable_LegacyHidden;
    
  25.     Activity = React.unstable_Activity;
    
  26.     Suspense = React.Suspense;
    
  27.     useState = React.useState;
    
  28.     useEffect = React.useEffect;
    
  29.     startTransition = React.startTransition;
    
  30. 
    
  31.     const InternalTestUtils = require('internal-test-utils');
    
  32.     waitFor = InternalTestUtils.waitFor;
    
  33.     waitForPaint = InternalTestUtils.waitForPaint;
    
  34.     assertLog = InternalTestUtils.assertLog;
    
  35. 
    
  36.     textCache = new Map();
    
  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.   // @gate enableActivity
    
  102.   test('basic example of suspending inside hidden tree', async () => {
    
  103.     const root = ReactNoop.createRoot();
    
  104. 
    
  105.     function App() {
    
  106.       return (
    
  107.         <Suspense fallback={<Text text="Loading..." />}>
    
  108.           <span>
    
  109.             <Text text="Visible" />
    
  110.           </span>
    
  111.           <Activity mode="hidden">
    
  112.             <span>
    
  113.               <AsyncText text="Hidden" />
    
  114.             </span>
    
  115.           </Activity>
    
  116.         </Suspense>
    
  117.       );
    
  118.     }
    
  119. 
    
  120.     // The hidden tree hasn't finished loading, but we should still be able to
    
  121.     // show the surrounding contents. The outer Suspense boundary
    
  122.     // isn't affected.
    
  123.     await act(() => {
    
  124.       root.render(<App />);
    
  125.     });
    
  126.     assertLog(['Visible', 'Suspend! [Hidden]']);
    
  127.     expect(root).toMatchRenderedOutput(<span>Visible</span>);
    
  128. 
    
  129.     // When the data resolves, we should be able to finish prerendering
    
  130.     // the hidden tree.
    
  131.     await act(async () => {
    
  132.       await resolveText('Hidden');
    
  133.     });
    
  134.     assertLog(['Hidden']);
    
  135.     expect(root).toMatchRenderedOutput(
    
  136.       <>
    
  137.         <span>Visible</span>
    
  138.         <span hidden={true}>Hidden</span>
    
  139.       </>,
    
  140.     );
    
  141.   });
    
  142. 
    
  143.   // @gate www
    
  144.   test('LegacyHidden does not handle suspense', async () => {
    
  145.     const root = ReactNoop.createRoot();
    
  146. 
    
  147.     function App() {
    
  148.       return (
    
  149.         <Suspense fallback={<Text text="Loading..." />}>
    
  150.           <span>
    
  151.             <Text text="Visible" />
    
  152.           </span>
    
  153.           <LegacyHidden mode="hidden">
    
  154.             <span>
    
  155.               <AsyncText text="Hidden" />
    
  156.             </span>
    
  157.           </LegacyHidden>
    
  158.         </Suspense>
    
  159.       );
    
  160.     }
    
  161. 
    
  162.     // Unlike Activity, LegacyHidden never captures if something suspends
    
  163.     await act(() => {
    
  164.       root.render(<App />);
    
  165.     });
    
  166.     assertLog(['Visible', 'Suspend! [Hidden]', 'Loading...']);
    
  167.     // Nearest Suspense boundary switches to a fallback even though the
    
  168.     // suspended content is hidden.
    
  169.     expect(root).toMatchRenderedOutput(
    
  170.       <>
    
  171.         <span hidden={true}>Visible</span>
    
  172.         Loading...
    
  173.       </>,
    
  174.     );
    
  175.   });
    
  176. 
    
  177.   // @gate experimental || www
    
  178.   test("suspending inside currently hidden tree that's switching to visible", async () => {
    
  179.     const root = ReactNoop.createRoot();
    
  180. 
    
  181.     function Details({open, children}) {
    
  182.       return (
    
  183.         <Suspense fallback={<Text text="Loading..." />}>
    
  184.           <span>
    
  185.             <Text text={open ? 'Open' : 'Closed'} />
    
  186.           </span>
    
  187.           <Activity mode={open ? 'visible' : 'hidden'}>
    
  188.             <span>{children}</span>
    
  189.           </Activity>
    
  190.         </Suspense>
    
  191.       );
    
  192.     }
    
  193. 
    
  194.     // The hidden tree hasn't finished loading, but we should still be able to
    
  195.     // show the surrounding contents. It doesn't matter that there's no
    
  196.     // Suspense boundary because the unfinished content isn't visible.
    
  197.     await act(() => {
    
  198.       root.render(
    
  199.         <Details open={false}>
    
  200.           <AsyncText text="Async" />
    
  201.         </Details>,
    
  202.       );
    
  203.     });
    
  204.     assertLog(['Closed', 'Suspend! [Async]']);
    
  205.     expect(root).toMatchRenderedOutput(<span>Closed</span>);
    
  206. 
    
  207.     // But when we switch the boundary from hidden to visible, it should
    
  208.     // now bubble to the nearest Suspense boundary.
    
  209.     await act(() => {
    
  210.       startTransition(() => {
    
  211.         root.render(
    
  212.           <Details open={true}>
    
  213.             <AsyncText text="Async" />
    
  214.           </Details>,
    
  215.         );
    
  216.       });
    
  217.     });
    
  218.     assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
    
  219.     // It should suspend with delay to prevent the already-visible Suspense
    
  220.     // boundary from switching to a fallback
    
  221.     expect(root).toMatchRenderedOutput(<span>Closed</span>);
    
  222. 
    
  223.     // Resolve the data and finish rendering
    
  224.     await act(async () => {
    
  225.       await resolveText('Async');
    
  226.     });
    
  227.     assertLog(['Open', 'Async']);
    
  228.     expect(root).toMatchRenderedOutput(
    
  229.       <>
    
  230.         <span>Open</span>
    
  231.         <span>Async</span>
    
  232.       </>,
    
  233.     );
    
  234.   });
    
  235. 
    
  236.   // @gate enableActivity
    
  237.   test("suspending inside currently visible tree that's switching to hidden", async () => {
    
  238.     const root = ReactNoop.createRoot();
    
  239. 
    
  240.     function Details({open, children}) {
    
  241.       return (
    
  242.         <Suspense fallback={<Text text="Loading..." />}>
    
  243.           <span>
    
  244.             <Text text={open ? 'Open' : 'Closed'} />
    
  245.           </span>
    
  246.           <Activity mode={open ? 'visible' : 'hidden'}>
    
  247.             <span>{children}</span>
    
  248.           </Activity>
    
  249.         </Suspense>
    
  250.       );
    
  251.     }
    
  252. 
    
  253.     // Initial mount. Nothing suspends
    
  254.     await act(() => {
    
  255.       root.render(
    
  256.         <Details open={true}>
    
  257.           <Text text="(empty)" />
    
  258.         </Details>,
    
  259.       );
    
  260.     });
    
  261.     assertLog(['Open', '(empty)']);
    
  262.     expect(root).toMatchRenderedOutput(
    
  263.       <>
    
  264.         <span>Open</span>
    
  265.         <span>(empty)</span>
    
  266.       </>,
    
  267.     );
    
  268. 
    
  269.     // Update that suspends inside the currently visible tree
    
  270.     await act(() => {
    
  271.       startTransition(() => {
    
  272.         root.render(
    
  273.           <Details open={true}>
    
  274.             <AsyncText text="Async" />
    
  275.           </Details>,
    
  276.         );
    
  277.       });
    
  278.     });
    
  279.     assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
    
  280.     // It should suspend with delay to prevent the already-visible Suspense
    
  281.     // boundary from switching to a fallback
    
  282.     expect(root).toMatchRenderedOutput(
    
  283.       <>
    
  284.         <span>Open</span>
    
  285.         <span>(empty)</span>
    
  286.       </>,
    
  287.     );
    
  288. 
    
  289.     // Update that hides the suspended tree
    
  290.     await act(() => {
    
  291.       startTransition(() => {
    
  292.         root.render(
    
  293.           <Details open={false}>
    
  294.             <AsyncText text="Async" />
    
  295.           </Details>,
    
  296.         );
    
  297.       });
    
  298.     });
    
  299.     // Now the visible part of the tree can commit without being blocked
    
  300.     // by the suspended content, which is hidden.
    
  301.     assertLog(['Closed', 'Suspend! [Async]']);
    
  302.     expect(root).toMatchRenderedOutput(
    
  303.       <>
    
  304.         <span>Closed</span>
    
  305.         <span hidden={true}>(empty)</span>
    
  306.       </>,
    
  307.     );
    
  308. 
    
  309.     // Resolve the data and finish rendering
    
  310.     await act(async () => {
    
  311.       await resolveText('Async');
    
  312.     });
    
  313.     assertLog(['Async']);
    
  314.     expect(root).toMatchRenderedOutput(
    
  315.       <>
    
  316.         <span>Closed</span>
    
  317.         <span hidden={true}>Async</span>
    
  318.       </>,
    
  319.     );
    
  320.   });
    
  321. 
    
  322.   // @gate experimental || www
    
  323.   test('update that suspends inside hidden tree', async () => {
    
  324.     let setText;
    
  325.     function Child() {
    
  326.       const [text, _setText] = useState('A');
    
  327.       setText = _setText;
    
  328.       return <AsyncText text={text} />;
    
  329.     }
    
  330. 
    
  331.     function App({show}) {
    
  332.       return (
    
  333.         <Activity mode={show ? 'visible' : 'hidden'}>
    
  334.           <span>
    
  335.             <Child />
    
  336.           </span>
    
  337.         </Activity>
    
  338.       );
    
  339.     }
    
  340. 
    
  341.     const root = ReactNoop.createRoot();
    
  342.     resolveText('A');
    
  343.     await act(() => {
    
  344.       root.render(<App show={false} />);
    
  345.     });
    
  346.     assertLog(['A']);
    
  347. 
    
  348.     await act(() => {
    
  349.       startTransition(() => {
    
  350.         setText('B');
    
  351.       });
    
  352.     });
    
  353.   });
    
  354. 
    
  355.   // @gate experimental || www
    
  356.   test('updates at multiple priorities that suspend inside hidden tree', async () => {
    
  357.     let setText;
    
  358.     let setStep;
    
  359.     function Child() {
    
  360.       const [text, _setText] = useState('A');
    
  361.       setText = _setText;
    
  362. 
    
  363.       const [step, _setStep] = useState(0);
    
  364.       setStep = _setStep;
    
  365. 
    
  366.       return <AsyncText text={text + step} />;
    
  367.     }
    
  368. 
    
  369.     function App({show}) {
    
  370.       return (
    
  371.         <Activity mode={show ? 'visible' : 'hidden'}>
    
  372.           <span>
    
  373.             <Child />
    
  374.           </span>
    
  375.         </Activity>
    
  376.       );
    
  377.     }
    
  378. 
    
  379.     const root = ReactNoop.createRoot();
    
  380.     resolveText('A0');
    
  381.     await act(() => {
    
  382.       root.render(<App show={false} />);
    
  383.     });
    
  384.     assertLog(['A0']);
    
  385.     expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
    
  386. 
    
  387.     await act(() => {
    
  388.       React.startTransition(() => {
    
  389.         setStep(1);
    
  390.       });
    
  391.       ReactNoop.flushSync(() => {
    
  392.         setText('B');
    
  393.       });
    
  394.     });
    
  395.     assertLog([
    
  396.       // The high priority render suspends again
    
  397.       'Suspend! [B0]',
    
  398.       // There's still pending work in another lane, so we should attempt
    
  399.       // that, too.
    
  400.       'Suspend! [B1]',
    
  401.     ]);
    
  402.     expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
    
  403. 
    
  404.     // Resolve the data and finish rendering
    
  405.     await act(() => {
    
  406.       resolveText('B1');
    
  407.     });
    
  408.     assertLog(['B1']);
    
  409.     expect(root).toMatchRenderedOutput(<span hidden={true}>B1</span>);
    
  410.   });
    
  411. 
    
  412.   // @gate enableActivity
    
  413.   test('detect updates to a hidden tree during a concurrent event', async () => {
    
  414.     // This is a pretty complex test case. It relates to how we detect if an
    
  415.     // update is made to a hidden tree: when scheduling the update, we walk up
    
  416.     // the fiber return path to see if any of the parents is a hidden Activity
    
  417.     // component. This doesn't work if there's already a render in progress,
    
  418.     // because the tree might be about to flip to hidden. To avoid a data race,
    
  419.     // queue updates atomically: wait to queue the update until after the
    
  420.     // current render has finished.
    
  421. 
    
  422.     let setInner;
    
  423.     function Child({outer}) {
    
  424.       const [inner, _setInner] = useState(0);
    
  425.       setInner = _setInner;
    
  426. 
    
  427.       useEffect(() => {
    
  428.         // Inner and outer values are always updated simultaneously, so they
    
  429.         // should always be consistent.
    
  430.         if (inner !== outer) {
    
  431.           Scheduler.log('Tearing! Inner and outer are inconsistent!');
    
  432.         } else {
    
  433.           Scheduler.log('Inner and outer are consistent');
    
  434.         }
    
  435.       }, [inner, outer]);
    
  436. 
    
  437.       return <Text text={'Inner: ' + inner} />;
    
  438.     }
    
  439. 
    
  440.     let setOuter;
    
  441.     function App({show}) {
    
  442.       const [outer, _setOuter] = useState(0);
    
  443.       setOuter = _setOuter;
    
  444.       return (
    
  445.         <>
    
  446.           <Activity mode={show ? 'visible' : 'hidden'}>
    
  447.             <span>
    
  448.               <Child outer={outer} />
    
  449.             </span>
    
  450.           </Activity>
    
  451.           <span>
    
  452.             <Text text={'Outer: ' + outer} />
    
  453.           </span>
    
  454.           <Suspense fallback={<Text text="Loading..." />}>
    
  455.             <span>
    
  456.               <Text text={'Sibling: ' + outer} />
    
  457.             </span>
    
  458.           </Suspense>
    
  459.         </>
    
  460.       );
    
  461.     }
    
  462. 
    
  463.     // Render a hidden tree
    
  464.     const root = ReactNoop.createRoot();
    
  465.     resolveText('Async: 0');
    
  466.     await act(() => {
    
  467.       root.render(<App show={true} />);
    
  468.     });
    
  469.     assertLog([
    
  470.       'Inner: 0',
    
  471.       'Outer: 0',
    
  472.       'Sibling: 0',
    
  473.       'Inner and outer are consistent',
    
  474.     ]);
    
  475.     expect(root).toMatchRenderedOutput(
    
  476.       <>
    
  477.         <span>Inner: 0</span>
    
  478.         <span>Outer: 0</span>
    
  479.         <span>Sibling: 0</span>
    
  480.       </>,
    
  481.     );
    
  482. 
    
  483.     await act(async () => {
    
  484.       // Update a value both inside and outside the hidden tree. These values
    
  485.       // must always be consistent.
    
  486.       startTransition(() => {
    
  487.         setOuter(1);
    
  488.         setInner(1);
    
  489.         // In the same render, also hide the offscreen tree.
    
  490.         root.render(<App show={false} />);
    
  491.       });
    
  492. 
    
  493.       await waitFor([
    
  494.         // The outer update will commit, but the inner update is deferred until
    
  495.         // a later render.
    
  496.         'Outer: 1',
    
  497.       ]);
    
  498. 
    
  499.       // Assert that we haven't committed quite yet
    
  500.       expect(root).toMatchRenderedOutput(
    
  501.         <>
    
  502.           <span>Inner: 0</span>
    
  503.           <span>Outer: 0</span>
    
  504.           <span>Sibling: 0</span>
    
  505.         </>,
    
  506.       );
    
  507. 
    
  508.       // Before the tree commits, schedule a concurrent event. The inner update
    
  509.       // is to a tree that's just about to be hidden.
    
  510.       startTransition(() => {
    
  511.         setOuter(2);
    
  512.         setInner(2);
    
  513.       });
    
  514. 
    
  515.       // Finish rendering and commit the in-progress render.
    
  516.       await waitForPaint(['Sibling: 1']);
    
  517.       expect(root).toMatchRenderedOutput(
    
  518.         <>
    
  519.           <span hidden={true}>Inner: 0</span>
    
  520.           <span>Outer: 1</span>
    
  521.           <span>Sibling: 1</span>
    
  522.         </>,
    
  523.       );
    
  524. 
    
  525.       // Now reveal the hidden tree at high priority.
    
  526.       ReactNoop.flushSync(() => {
    
  527.         root.render(<App show={true} />);
    
  528.       });
    
  529.       assertLog([
    
  530.         // There are two pending updates on Inner, but only the first one
    
  531.         // is processed, even though they share the same lane. If the second
    
  532.         // update were erroneously processed, then Inner would be inconsistent
    
  533.         // with Outer.
    
  534.         'Inner: 1',
    
  535.         'Outer: 1',
    
  536.         'Sibling: 1',
    
  537.         'Inner and outer are consistent',
    
  538.       ]);
    
  539.     });
    
  540.     assertLog([
    
  541.       'Inner: 2',
    
  542.       'Outer: 2',
    
  543.       'Sibling: 2',
    
  544.       'Inner and outer are consistent',
    
  545.     ]);
    
  546.     expect(root).toMatchRenderedOutput(
    
  547.       <>
    
  548.         <span>Inner: 2</span>
    
  549.         <span>Outer: 2</span>
    
  550.         <span>Sibling: 2</span>
    
  551.       </>,
    
  552.     );
    
  553.   });
    
  554. });