1. let React;
    
  2. let Fragment;
    
  3. let ReactNoop;
    
  4. let Scheduler;
    
  5. let act;
    
  6. let waitFor;
    
  7. let waitForAll;
    
  8. let assertLog;
    
  9. let waitForPaint;
    
  10. let Suspense;
    
  11. let startTransition;
    
  12. let getCacheForType;
    
  13. 
    
  14. let caches;
    
  15. let seededCache;
    
  16. 
    
  17. describe('ReactSuspenseWithNoopRenderer', () => {
    
  18.   beforeEach(() => {
    
  19.     jest.resetModules();
    
  20. 
    
  21.     React = require('react');
    
  22.     Fragment = React.Fragment;
    
  23.     ReactNoop = require('react-noop-renderer');
    
  24.     Scheduler = require('scheduler');
    
  25.     act = require('internal-test-utils').act;
    
  26.     Suspense = React.Suspense;
    
  27.     startTransition = React.startTransition;
    
  28.     const InternalTestUtils = require('internal-test-utils');
    
  29.     waitFor = InternalTestUtils.waitFor;
    
  30.     waitForAll = InternalTestUtils.waitForAll;
    
  31.     waitForPaint = InternalTestUtils.waitForPaint;
    
  32.     assertLog = InternalTestUtils.assertLog;
    
  33. 
    
  34.     getCacheForType = React.unstable_getCacheForType;
    
  35. 
    
  36.     caches = [];
    
  37.     seededCache = null;
    
  38.   });
    
  39. 
    
  40.   function createTextCache() {
    
  41.     if (seededCache !== null) {
    
  42.       // Trick to seed a cache before it exists.
    
  43.       // TODO: Need a built-in API to seed data before the initial render (i.e.
    
  44.       // not a refresh because nothing has mounted yet).
    
  45.       const cache = seededCache;
    
  46.       seededCache = null;
    
  47.       return cache;
    
  48.     }
    
  49. 
    
  50.     const data = new Map();
    
  51.     const version = caches.length + 1;
    
  52.     const cache = {
    
  53.       version,
    
  54.       data,
    
  55.       resolve(text) {
    
  56.         const record = data.get(text);
    
  57.         if (record === undefined) {
    
  58.           const newRecord = {
    
  59.             status: 'resolved',
    
  60.             value: text,
    
  61.           };
    
  62.           data.set(text, newRecord);
    
  63.         } else if (record.status === 'pending') {
    
  64.           const thenable = record.value;
    
  65.           record.status = 'resolved';
    
  66.           record.value = text;
    
  67.           thenable.pings.forEach(t => t());
    
  68.         }
    
  69.       },
    
  70.       reject(text, error) {
    
  71.         const record = data.get(text);
    
  72.         if (record === undefined) {
    
  73.           const newRecord = {
    
  74.             status: 'rejected',
    
  75.             value: error,
    
  76.           };
    
  77.           data.set(text, newRecord);
    
  78.         } else if (record.status === 'pending') {
    
  79.           const thenable = record.value;
    
  80.           record.status = 'rejected';
    
  81.           record.value = error;
    
  82.           thenable.pings.forEach(t => t());
    
  83.         }
    
  84.       },
    
  85.     };
    
  86.     caches.push(cache);
    
  87.     return cache;
    
  88.   }
    
  89. 
    
  90.   function readText(text) {
    
  91.     const textCache = getCacheForType(createTextCache);
    
  92.     const record = textCache.data.get(text);
    
  93.     if (record !== undefined) {
    
  94.       switch (record.status) {
    
  95.         case 'pending':
    
  96.           Scheduler.log(`Suspend! [${text}]`);
    
  97.           throw record.value;
    
  98.         case 'rejected':
    
  99.           Scheduler.log(`Error! [${text}]`);
    
  100.           throw record.value;
    
  101.         case 'resolved':
    
  102.           return textCache.version;
    
  103.       }
    
  104.     } else {
    
  105.       Scheduler.log(`Suspend! [${text}]`);
    
  106. 
    
  107.       const thenable = {
    
  108.         pings: [],
    
  109.         then(resolve) {
    
  110.           if (newRecord.status === 'pending') {
    
  111.             thenable.pings.push(resolve);
    
  112.           } else {
    
  113.             Promise.resolve().then(() => resolve(newRecord.value));
    
  114.           }
    
  115.         },
    
  116.       };
    
  117. 
    
  118.       const newRecord = {
    
  119.         status: 'pending',
    
  120.         value: thenable,
    
  121.       };
    
  122.       textCache.data.set(text, newRecord);
    
  123. 
    
  124.       throw thenable;
    
  125.     }
    
  126.   }
    
  127. 
    
  128.   function Text({text}) {
    
  129.     Scheduler.log(text);
    
  130.     return <span prop={text} />;
    
  131.   }
    
  132. 
    
  133.   function AsyncText({text, showVersion}) {
    
  134.     const version = readText(text);
    
  135.     const fullText = showVersion ? `${text} [v${version}]` : text;
    
  136.     Scheduler.log(fullText);
    
  137.     return <span prop={fullText} />;
    
  138.   }
    
  139. 
    
  140.   function seedNextTextCache(text) {
    
  141.     if (seededCache === null) {
    
  142.       seededCache = createTextCache();
    
  143.     }
    
  144.     seededCache.resolve(text);
    
  145.   }
    
  146. 
    
  147.   function resolveMostRecentTextCache(text) {
    
  148.     if (caches.length === 0) {
    
  149.       throw Error('Cache does not exist.');
    
  150.     } else {
    
  151.       // Resolve the most recently created cache. An older cache can by
    
  152.       // resolved with `caches[index].resolve(text)`.
    
  153.       caches[caches.length - 1].resolve(text);
    
  154.     }
    
  155.   }
    
  156. 
    
  157.   const resolveText = resolveMostRecentTextCache;
    
  158. 
    
  159.   function rejectMostRecentTextCache(text, error) {
    
  160.     if (caches.length === 0) {
    
  161.       throw Error('Cache does not exist.');
    
  162.     } else {
    
  163.       // Resolve the most recently created cache. An older cache can by
    
  164.       // resolved with `caches[index].reject(text, error)`.
    
  165.       caches[caches.length - 1].reject(text, error);
    
  166.     }
    
  167.   }
    
  168. 
    
  169.   const rejectText = rejectMostRecentTextCache;
    
  170. 
    
  171.   function advanceTimers(ms) {
    
  172.     // Note: This advances Jest's virtual time but not React's. Use
    
  173.     // ReactNoop.expire for that.
    
  174.     if (typeof ms !== 'number') {
    
  175.       throw new Error('Must specify ms');
    
  176.     }
    
  177.     jest.advanceTimersByTime(ms);
    
  178.     // Wait until the end of the current tick
    
  179.     // We cannot use a timer since we're faking them
    
  180.     return Promise.resolve().then(() => {});
    
  181.   }
    
  182. 
    
  183.   // Note: This is based on a similar component we use in www. We can delete
    
  184.   // once the extra div wrapper is no longer necessary.
    
  185.   function LegacyHiddenDiv({children, mode}) {
    
  186.     return (
    
  187.       <div hidden={mode === 'hidden'}>
    
  188.         <React.unstable_LegacyHidden
    
  189.           mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
    
  190.           {children}
    
  191.         </React.unstable_LegacyHidden>
    
  192.       </div>
    
  193.     );
    
  194.   }
    
  195. 
    
  196.   // @gate enableLegacyCache
    
  197.   it("does not restart if there's a ping during initial render", async () => {
    
  198.     function Bar(props) {
    
  199.       Scheduler.log('Bar');
    
  200.       return props.children;
    
  201.     }
    
  202. 
    
  203.     function Foo() {
    
  204.       Scheduler.log('Foo');
    
  205.       return (
    
  206.         <>
    
  207.           <Suspense fallback={<Text text="Loading..." />}>
    
  208.             <Bar>
    
  209.               <AsyncText text="A" ms={100} />
    
  210.               <Text text="B" />
    
  211.             </Bar>
    
  212.           </Suspense>
    
  213.           <Text text="C" />
    
  214.           <Text text="D" />
    
  215.         </>
    
  216.       );
    
  217.     }
    
  218. 
    
  219.     React.startTransition(() => {
    
  220.       ReactNoop.render(<Foo />);
    
  221.     });
    
  222.     await waitFor([
    
  223.       'Foo',
    
  224.       'Bar',
    
  225.       // A suspends
    
  226.       'Suspend! [A]',
    
  227.       // We immediately unwind and switch to a fallback without
    
  228.       // rendering siblings.
    
  229.       'Loading...',
    
  230.       'C',
    
  231.       // Yield before rendering D
    
  232.     ]);
    
  233.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  234. 
    
  235.     // Flush the promise completely
    
  236.     await act(async () => {
    
  237.       await resolveText('A');
    
  238.       // Even though the promise has resolved, we should now flush
    
  239.       // and commit the in progress render instead of restarting.
    
  240.       await waitForPaint(['D']);
    
  241.       expect(ReactNoop).toMatchRenderedOutput(
    
  242.         <>
    
  243.           <span prop="Loading..." />
    
  244.           <span prop="C" />
    
  245.           <span prop="D" />
    
  246.         </>,
    
  247.       );
    
  248.       // Next, we'll flush the complete content.
    
  249.       await waitForAll(['Bar', 'A', 'B']);
    
  250.     });
    
  251. 
    
  252.     expect(ReactNoop).toMatchRenderedOutput(
    
  253.       <>
    
  254.         <span prop="A" />
    
  255.         <span prop="B" />
    
  256.         <span prop="C" />
    
  257.         <span prop="D" />
    
  258.       </>,
    
  259.     );
    
  260.   });
    
  261. 
    
  262.   // @gate enableLegacyCache
    
  263.   it('suspends rendering and continues later', async () => {
    
  264.     function Bar(props) {
    
  265.       Scheduler.log('Bar');
    
  266.       return props.children;
    
  267.     }
    
  268. 
    
  269.     function Foo({renderBar}) {
    
  270.       Scheduler.log('Foo');
    
  271.       return (
    
  272.         <Suspense fallback={<Text text="Loading..." />}>
    
  273.           {renderBar ? (
    
  274.             <Bar>
    
  275.               <AsyncText text="A" />
    
  276.               <Text text="B" />
    
  277.             </Bar>
    
  278.           ) : null}
    
  279.         </Suspense>
    
  280.       );
    
  281.     }
    
  282. 
    
  283.     // Render empty shell.
    
  284.     ReactNoop.render(<Foo />);
    
  285.     await waitForAll(['Foo']);
    
  286. 
    
  287.     // The update will suspend.
    
  288.     React.startTransition(() => {
    
  289.       ReactNoop.render(<Foo renderBar={true} />);
    
  290.     });
    
  291.     await waitForAll([
    
  292.       'Foo',
    
  293.       'Bar',
    
  294.       // A suspends
    
  295.       'Suspend! [A]',
    
  296.       // We immediately unwind and switch to a fallback without
    
  297.       // rendering siblings.
    
  298.       'Loading...',
    
  299.     ]);
    
  300.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  301. 
    
  302.     // Resolve the data
    
  303.     await resolveText('A');
    
  304.     // Renders successfully
    
  305.     await waitForAll(['Foo', 'Bar', 'A', 'B']);
    
  306.     expect(ReactNoop).toMatchRenderedOutput(
    
  307.       <>
    
  308.         <span prop="A" />
    
  309.         <span prop="B" />
    
  310.       </>,
    
  311.     );
    
  312.   });
    
  313. 
    
  314.   // @gate enableLegacyCache
    
  315.   it('suspends siblings and later recovers each independently', async () => {
    
  316.     // Render two sibling Suspense components
    
  317.     ReactNoop.render(
    
  318.       <Fragment>
    
  319.         <Suspense fallback={<Text text="Loading A..." />}>
    
  320.           <AsyncText text="A" />
    
  321.         </Suspense>
    
  322.         <Suspense fallback={<Text text="Loading B..." />}>
    
  323.           <AsyncText text="B" />
    
  324.         </Suspense>
    
  325.       </Fragment>,
    
  326.     );
    
  327.     await waitForAll([
    
  328.       'Suspend! [A]',
    
  329.       'Loading A...',
    
  330.       'Suspend! [B]',
    
  331.       'Loading B...',
    
  332.     ]);
    
  333.     expect(ReactNoop).toMatchRenderedOutput(
    
  334.       <>
    
  335.         <span prop="Loading A..." />
    
  336.         <span prop="Loading B..." />
    
  337.       </>,
    
  338.     );
    
  339. 
    
  340.     // Resolve first Suspense's promise so that it switches switches back to the
    
  341.     // normal view. The second Suspense should still show the placeholder.
    
  342.     await act(() => resolveText('A'));
    
  343.     assertLog(['A']);
    
  344.     expect(ReactNoop).toMatchRenderedOutput(
    
  345.       <>
    
  346.         <span prop="A" />
    
  347.         <span prop="Loading B..." />
    
  348.       </>,
    
  349.     );
    
  350. 
    
  351.     // Resolve the second Suspense's promise so that it switches back to the
    
  352.     // normal view.
    
  353.     await act(() => resolveText('B'));
    
  354.     assertLog(['B']);
    
  355.     expect(ReactNoop).toMatchRenderedOutput(
    
  356.       <>
    
  357.         <span prop="A" />
    
  358.         <span prop="B" />
    
  359.       </>,
    
  360.     );
    
  361.   });
    
  362. 
    
  363.   // @gate enableLegacyCache
    
  364.   it('when something suspends, unwinds immediately without rendering siblings', async () => {
    
  365.     // A shell is needed. The update cause it to suspend.
    
  366.     ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
    
  367.     await waitForAll([]);
    
  368.     React.startTransition(() => {
    
  369.       ReactNoop.render(
    
  370.         <Suspense fallback={<Text text="Loading..." />}>
    
  371.           <Text text="A" />
    
  372.           <AsyncText text="B" />
    
  373.           <Text text="C" />
    
  374.           <Text text="D" />
    
  375.         </Suspense>,
    
  376.       );
    
  377.     });
    
  378. 
    
  379.     // B suspends. Render a fallback
    
  380.     await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
    
  381.     // Did not commit yet.
    
  382.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  383. 
    
  384.     // Wait for data to resolve
    
  385.     await resolveText('B');
    
  386.     await waitForAll(['A', 'B', 'C', 'D']);
    
  387.     // Renders successfully
    
  388.     expect(ReactNoop).toMatchRenderedOutput(
    
  389.       <>
    
  390.         <span prop="A" />
    
  391.         <span prop="B" />
    
  392.         <span prop="C" />
    
  393.         <span prop="D" />
    
  394.       </>,
    
  395.     );
    
  396.   });
    
  397. 
    
  398.   // Second condition is redundant but guarantees that the test runs in prod.
    
  399.   // TODO: Delete this feature flag.
    
  400.   // @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
    
  401.   // @gate enableLegacyCache
    
  402.   it('retries on error', async () => {
    
  403.     class ErrorBoundary extends React.Component {
    
  404.       state = {error: null};
    
  405.       componentDidCatch(error) {
    
  406.         this.setState({error});
    
  407.       }
    
  408.       reset() {
    
  409.         this.setState({error: null});
    
  410.       }
    
  411.       render() {
    
  412.         if (this.state.error !== null) {
    
  413.           return <Text text={'Caught error: ' + this.state.error.message} />;
    
  414.         }
    
  415.         return this.props.children;
    
  416.       }
    
  417.     }
    
  418. 
    
  419.     const errorBoundary = React.createRef();
    
  420.     function App({renderContent}) {
    
  421.       return (
    
  422.         <Suspense fallback={<Text text="Loading..." />}>
    
  423.           {renderContent ? (
    
  424.             <ErrorBoundary ref={errorBoundary}>
    
  425.               <AsyncText text="Result" ms={1000} />
    
  426.             </ErrorBoundary>
    
  427.           ) : null}
    
  428.         </Suspense>
    
  429.       );
    
  430.     }
    
  431. 
    
  432.     ReactNoop.render(<App />);
    
  433.     await waitForAll([]);
    
  434.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  435. 
    
  436.     React.startTransition(() => {
    
  437.       ReactNoop.render(<App renderContent={true} />);
    
  438.     });
    
  439.     await waitForAll(['Suspend! [Result]', 'Loading...']);
    
  440.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  441. 
    
  442.     await rejectText('Result', new Error('Failed to load: Result'));
    
  443. 
    
  444.     await waitForAll([
    
  445.       'Error! [Result]',
    
  446. 
    
  447.       // React retries one more time
    
  448.       'Error! [Result]',
    
  449. 
    
  450.       // Errored again on retry. Now handle it.
    
  451.       'Caught error: Failed to load: Result',
    
  452.     ]);
    
  453.     expect(ReactNoop).toMatchRenderedOutput(
    
  454.       <span prop="Caught error: Failed to load: Result" />,
    
  455.     );
    
  456.   });
    
  457. 
    
  458.   // Second condition is redundant but guarantees that the test runs in prod.
    
  459.   // TODO: Delete this feature flag.
    
  460.   // @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
    
  461.   // @gate enableLegacyCache
    
  462.   it('retries on error after falling back to a placeholder', async () => {
    
  463.     class ErrorBoundary extends React.Component {
    
  464.       state = {error: null};
    
  465.       componentDidCatch(error) {
    
  466.         this.setState({error});
    
  467.       }
    
  468.       reset() {
    
  469.         this.setState({error: null});
    
  470.       }
    
  471.       render() {
    
  472.         if (this.state.error !== null) {
    
  473.           return <Text text={'Caught error: ' + this.state.error.message} />;
    
  474.         }
    
  475.         return this.props.children;
    
  476.       }
    
  477.     }
    
  478. 
    
  479.     const errorBoundary = React.createRef();
    
  480.     function App() {
    
  481.       return (
    
  482.         <Suspense fallback={<Text text="Loading..." />}>
    
  483.           <ErrorBoundary ref={errorBoundary}>
    
  484.             <AsyncText text="Result" />
    
  485.           </ErrorBoundary>
    
  486.         </Suspense>
    
  487.       );
    
  488.     }
    
  489. 
    
  490.     ReactNoop.render(<App />);
    
  491.     await waitForAll(['Suspend! [Result]', 'Loading...']);
    
  492.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  493. 
    
  494.     await act(() => rejectText('Result', new Error('Failed to load: Result')));
    
  495.     assertLog([
    
  496.       'Error! [Result]',
    
  497. 
    
  498.       // React retries one more time
    
  499.       'Error! [Result]',
    
  500. 
    
  501.       // Errored again on retry. Now handle it.
    
  502.       'Caught error: Failed to load: Result',
    
  503.     ]);
    
  504.     expect(ReactNoop).toMatchRenderedOutput(
    
  505.       <span prop="Caught error: Failed to load: Result" />,
    
  506.     );
    
  507.   });
    
  508. 
    
  509.   // @gate enableLegacyCache
    
  510.   it('can update at a higher priority while in a suspended state', async () => {
    
  511.     let setHighPri;
    
  512.     function HighPri() {
    
  513.       const [text, setText] = React.useState('A');
    
  514.       setHighPri = setText;
    
  515.       return <Text text={text} />;
    
  516.     }
    
  517. 
    
  518.     let setLowPri;
    
  519.     function LowPri() {
    
  520.       const [text, setText] = React.useState('1');
    
  521.       setLowPri = setText;
    
  522.       return <AsyncText text={text} />;
    
  523.     }
    
  524. 
    
  525.     function App() {
    
  526.       return (
    
  527.         <>
    
  528.           <HighPri />
    
  529.           <Suspense fallback={<Text text="Loading..." />}>
    
  530.             <LowPri />
    
  531.           </Suspense>
    
  532.         </>
    
  533.       );
    
  534.     }
    
  535. 
    
  536.     // Initial mount
    
  537.     await act(() => ReactNoop.render(<App />));
    
  538.     assertLog(['A', 'Suspend! [1]', 'Loading...']);
    
  539. 
    
  540.     await act(() => resolveText('1'));
    
  541.     assertLog(['1']);
    
  542.     expect(ReactNoop).toMatchRenderedOutput(
    
  543.       <>
    
  544.         <span prop="A" />
    
  545.         <span prop="1" />
    
  546.       </>,
    
  547.     );
    
  548. 
    
  549.     // Update the low-pri text
    
  550.     await act(() => startTransition(() => setLowPri('2')));
    
  551.     // Suspends
    
  552.     assertLog(['Suspend! [2]', 'Loading...']);
    
  553. 
    
  554.     // While we're still waiting for the low-pri update to complete, update the
    
  555.     // high-pri text at high priority.
    
  556.     ReactNoop.flushSync(() => {
    
  557.       setHighPri('B');
    
  558.     });
    
  559.     assertLog(['B']);
    
  560.     expect(ReactNoop).toMatchRenderedOutput(
    
  561.       <>
    
  562.         <span prop="B" />
    
  563.         <span prop="1" />
    
  564.       </>,
    
  565.     );
    
  566. 
    
  567.     // Unblock the low-pri text and finish. Nothing in the UI changes because
    
  568.     // the update was overriden
    
  569.     await act(() => resolveText('2'));
    
  570.     assertLog(['2']);
    
  571.     expect(ReactNoop).toMatchRenderedOutput(
    
  572.       <>
    
  573.         <span prop="B" />
    
  574.         <span prop="2" />
    
  575.       </>,
    
  576.     );
    
  577.   });
    
  578. 
    
  579.   // @gate enableLegacyCache
    
  580.   it('keeps working on lower priority work after being pinged', async () => {
    
  581.     function App(props) {
    
  582.       return (
    
  583.         <Suspense fallback={<Text text="Loading..." />}>
    
  584.           {props.showA && <AsyncText text="A" />}
    
  585.           {props.showB && <Text text="B" />}
    
  586.         </Suspense>
    
  587.       );
    
  588.     }
    
  589. 
    
  590.     ReactNoop.render(<App showA={false} showB={false} />);
    
  591.     await waitForAll([]);
    
  592.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  593. 
    
  594.     React.startTransition(() => {
    
  595.       ReactNoop.render(<App showA={true} showB={false} />);
    
  596.     });
    
  597.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  598.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  599. 
    
  600.     React.startTransition(() => {
    
  601.       ReactNoop.render(<App showA={true} showB={true} />);
    
  602.     });
    
  603.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  604.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  605. 
    
  606.     await resolveText('A');
    
  607.     await waitForAll(['A', 'B']);
    
  608.     expect(ReactNoop).toMatchRenderedOutput(
    
  609.       <>
    
  610.         <span prop="A" />
    
  611.         <span prop="B" />
    
  612.       </>,
    
  613.     );
    
  614.   });
    
  615. 
    
  616.   // @gate enableLegacyCache
    
  617.   it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
    
  618.     function App(props) {
    
  619.       if (props.hide) {
    
  620.         return <Text text="(empty)" />;
    
  621.       }
    
  622.       return (
    
  623.         <Suspense fallback="Loading...">
    
  624.           <AsyncText ms={2000} text="Async" />
    
  625.         </Suspense>
    
  626.       );
    
  627.     }
    
  628. 
    
  629.     // Schedule a default pri update and a low pri update, without rendering in between.
    
  630.     // Default pri
    
  631.     ReactNoop.render(<App />);
    
  632.     // Low pri
    
  633.     React.startTransition(() => {
    
  634.       ReactNoop.render(<App hide={true} />);
    
  635.     });
    
  636. 
    
  637.     await waitForAll([
    
  638.       // The first update suspends
    
  639.       'Suspend! [Async]',
    
  640.       // but we have another pending update that we can work on
    
  641.       '(empty)',
    
  642.     ]);
    
  643.     expect(ReactNoop).toMatchRenderedOutput(<span prop="(empty)" />);
    
  644.   });
    
  645. 
    
  646.   // Note: This test was written to test a heuristic used in the expiration
    
  647.   // times model. Might not make sense in the new model.
    
  648.   // TODO: This test doesn't over what it was originally designed to test.
    
  649.   // Either rewrite or delete.
    
  650.   it('tries each subsequent level after suspending', async () => {
    
  651.     const root = ReactNoop.createRoot();
    
  652. 
    
  653.     function App({step, shouldSuspend}) {
    
  654.       return (
    
  655.         <Suspense fallback="Loading...">
    
  656.           <Text text="Sibling" />
    
  657.           {shouldSuspend ? (
    
  658.             <AsyncText text={'Step ' + step} />
    
  659.           ) : (
    
  660.             <Text text={'Step ' + step} />
    
  661.           )}
    
  662.         </Suspense>
    
  663.       );
    
  664.     }
    
  665. 
    
  666.     function interrupt() {
    
  667.       // React has a heuristic to batch all updates that occur within the same
    
  668.       // event. This is a trick to circumvent that heuristic.
    
  669.       ReactNoop.flushSync(() => {
    
  670.         ReactNoop.renderToRootWithID(null, 'other-root');
    
  671.       });
    
  672.     }
    
  673. 
    
  674.     // Mount the Suspense boundary without suspending, so that the subsequent
    
  675.     // updates suspend with a delay.
    
  676.     await act(() => {
    
  677.       root.render(<App step={0} shouldSuspend={false} />);
    
  678.     });
    
  679.     await advanceTimers(1000);
    
  680.     assertLog(['Sibling', 'Step 0']);
    
  681. 
    
  682.     // Schedule an update at several distinct expiration times
    
  683.     await act(async () => {
    
  684.       React.startTransition(() => {
    
  685.         root.render(<App step={1} shouldSuspend={true} />);
    
  686.       });
    
  687.       Scheduler.unstable_advanceTime(1000);
    
  688.       await waitFor(['Sibling']);
    
  689.       interrupt();
    
  690. 
    
  691.       React.startTransition(() => {
    
  692.         root.render(<App step={2} shouldSuspend={true} />);
    
  693.       });
    
  694.       Scheduler.unstable_advanceTime(1000);
    
  695.       await waitFor(['Sibling']);
    
  696.       interrupt();
    
  697. 
    
  698.       React.startTransition(() => {
    
  699.         root.render(<App step={3} shouldSuspend={true} />);
    
  700.       });
    
  701.       Scheduler.unstable_advanceTime(1000);
    
  702.       await waitFor(['Sibling']);
    
  703.       interrupt();
    
  704. 
    
  705.       root.render(<App step={4} shouldSuspend={false} />);
    
  706.     });
    
  707. 
    
  708.     assertLog(['Sibling', 'Step 4']);
    
  709.   });
    
  710. 
    
  711.   // @gate enableLegacyCache
    
  712.   it('switches to an inner fallback after suspending for a while', async () => {
    
  713.     // Advance the virtual time so that we're closer to the edge of a bucket.
    
  714.     ReactNoop.expire(200);
    
  715. 
    
  716.     ReactNoop.render(
    
  717.       <Fragment>
    
  718.         <Text text="Sync" />
    
  719.         <Suspense fallback={<Text text="Loading outer..." />}>
    
  720.           <AsyncText text="Outer content" ms={300} />
    
  721.           <Suspense fallback={<Text text="Loading inner..." />}>
    
  722.             <AsyncText text="Inner content" ms={1000} />
    
  723.           </Suspense>
    
  724.         </Suspense>
    
  725.       </Fragment>,
    
  726.     );
    
  727. 
    
  728.     await waitForAll([
    
  729.       'Sync',
    
  730.       // The async content suspends
    
  731.       'Suspend! [Outer content]',
    
  732.       'Loading outer...',
    
  733.     ]);
    
  734.     // The outer loading state finishes immediately.
    
  735.     expect(ReactNoop).toMatchRenderedOutput(
    
  736.       <>
    
  737.         <span prop="Sync" />
    
  738.         <span prop="Loading outer..." />
    
  739.       </>,
    
  740.     );
    
  741. 
    
  742.     // Resolve the outer promise.
    
  743.     await resolveText('Outer content');
    
  744.     await waitForAll([
    
  745.       'Outer content',
    
  746.       'Suspend! [Inner content]',
    
  747.       'Loading inner...',
    
  748.     ]);
    
  749.     // Don't commit the inner placeholder yet.
    
  750.     expect(ReactNoop).toMatchRenderedOutput(
    
  751.       <>
    
  752.         <span prop="Sync" />
    
  753.         <span prop="Loading outer..." />
    
  754.       </>,
    
  755.     );
    
  756. 
    
  757.     // Expire the inner timeout.
    
  758.     ReactNoop.expire(500);
    
  759.     await advanceTimers(500);
    
  760.     // Now that 750ms have elapsed since the outer placeholder timed out,
    
  761.     // we can timeout the inner placeholder.
    
  762.     expect(ReactNoop).toMatchRenderedOutput(
    
  763.       <>
    
  764.         <span prop="Sync" />
    
  765.         <span prop="Outer content" />
    
  766.         <span prop="Loading inner..." />
    
  767.       </>,
    
  768.     );
    
  769. 
    
  770.     // Finally, flush the inner promise. We should see the complete screen.
    
  771.     await act(() => resolveText('Inner content'));
    
  772.     assertLog(['Inner content']);
    
  773.     expect(ReactNoop).toMatchRenderedOutput(
    
  774.       <>
    
  775.         <span prop="Sync" />
    
  776.         <span prop="Outer content" />
    
  777.         <span prop="Inner content" />
    
  778.       </>,
    
  779.     );
    
  780.   });
    
  781. 
    
  782.   // @gate enableLegacyCache
    
  783.   it('renders an Suspense boundary synchronously', async () => {
    
  784.     spyOnDev(console, 'error');
    
  785.     // Synchronously render a tree that suspends
    
  786.     ReactNoop.flushSync(() =>
    
  787.       ReactNoop.render(
    
  788.         <Fragment>
    
  789.           <Suspense fallback={<Text text="Loading..." />}>
    
  790.             <AsyncText text="Async" />
    
  791.           </Suspense>
    
  792.           <Text text="Sync" />
    
  793.         </Fragment>,
    
  794.       ),
    
  795.     );
    
  796.     assertLog([
    
  797.       // The async child suspends
    
  798.       'Suspend! [Async]',
    
  799.       // We immediately render the fallback UI
    
  800.       'Loading...',
    
  801.       // Continue on the sibling
    
  802.       'Sync',
    
  803.     ]);
    
  804.     // The tree commits synchronously
    
  805.     expect(ReactNoop).toMatchRenderedOutput(
    
  806.       <>
    
  807.         <span prop="Loading..." />
    
  808.         <span prop="Sync" />
    
  809.       </>,
    
  810.     );
    
  811. 
    
  812.     // Once the promise resolves, we render the suspended view
    
  813.     await act(() => resolveText('Async'));
    
  814.     assertLog(['Async']);
    
  815.     expect(ReactNoop).toMatchRenderedOutput(
    
  816.       <>
    
  817.         <span prop="Async" />
    
  818.         <span prop="Sync" />
    
  819.       </>,
    
  820.     );
    
  821.   });
    
  822. 
    
  823.   // @gate enableLegacyCache
    
  824.   it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
    
  825.     ReactNoop.flushSync(() =>
    
  826.       ReactNoop.render(
    
  827.         <Fragment>
    
  828.           <Suspense fallback={<Text text="Loading (outer)..." />}>
    
  829.             <Suspense fallback={<AsyncText text="Loading (inner)..." />}>
    
  830.               <AsyncText text="Async" />
    
  831.             </Suspense>
    
  832.             <Text text="Sync" />
    
  833.           </Suspense>
    
  834.         </Fragment>,
    
  835.       ),
    
  836.     );
    
  837.     assertLog([
    
  838.       'Suspend! [Async]',
    
  839.       'Suspend! [Loading (inner)...]',
    
  840.       'Loading (outer)...',
    
  841.     ]);
    
  842.     // The tree commits synchronously
    
  843.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading (outer)..." />);
    
  844.   });
    
  845. 
    
  846.   // @gate enableLegacyCache
    
  847.   it('resolves successfully even if fallback render is pending', async () => {
    
  848.     const root = ReactNoop.createRoot();
    
  849.     root.render(
    
  850.       <>
    
  851.         <Suspense fallback={<Text text="Loading..." />} />
    
  852.       </>,
    
  853.     );
    
  854.     await waitForAll([]);
    
  855.     expect(root).toMatchRenderedOutput(null);
    
  856.     React.startTransition(() => {
    
  857.       root.render(
    
  858.         <>
    
  859.           <Suspense fallback={<Text text="Loading..." />}>
    
  860.             <AsyncText text="Async" />
    
  861.             <Text text="Sibling" />
    
  862.           </Suspense>
    
  863.         </>,
    
  864.       );
    
  865.     });
    
  866.     await waitFor(['Suspend! [Async]']);
    
  867. 
    
  868.     await resolveText('Async');
    
  869. 
    
  870.     // Because we're already showing a fallback, interrupt the current render
    
  871.     // and restart immediately.
    
  872.     await waitForAll(['Async', 'Sibling']);
    
  873.     expect(root).toMatchRenderedOutput(
    
  874.       <>
    
  875.         <span prop="Async" />
    
  876.         <span prop="Sibling" />
    
  877.       </>,
    
  878.     );
    
  879.   });
    
  880. 
    
  881.   // @gate enableLegacyCache
    
  882.   it('in concurrent mode, does not error when an update suspends without a Suspense boundary during a sync update', () => {
    
  883.     // NOTE: We may change this to be a warning in the future.
    
  884.     expect(() => {
    
  885.       ReactNoop.flushSync(() => {
    
  886.         ReactNoop.render(<AsyncText text="Async" />);
    
  887.       });
    
  888.     }).not.toThrow();
    
  889.   });
    
  890. 
    
  891.   // @gate enableLegacyCache
    
  892.   it('in legacy mode, errors when an update suspends without a Suspense boundary during a sync update', () => {
    
  893.     const root = ReactNoop.createLegacyRoot();
    
  894.     expect(() => root.render(<AsyncText text="Async" />)).toThrow(
    
  895.       'A component suspended while responding to synchronous input.',
    
  896.     );
    
  897.   });
    
  898. 
    
  899.   // @gate enableLegacyCache
    
  900.   it('a Suspense component correctly handles more than one suspended child', async () => {
    
  901.     ReactNoop.render(
    
  902.       <Suspense fallback={<Text text="Loading..." />}>
    
  903.         <AsyncText text="A" />
    
  904.         <AsyncText text="B" />
    
  905.       </Suspense>,
    
  906.     );
    
  907.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  908.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  909. 
    
  910.     await act(() => {
    
  911.       resolveText('A');
    
  912.       resolveText('B');
    
  913.     });
    
  914.     assertLog(['A', 'B']);
    
  915.     expect(ReactNoop).toMatchRenderedOutput(
    
  916.       <>
    
  917.         <span prop="A" />
    
  918.         <span prop="B" />
    
  919.       </>,
    
  920.     );
    
  921.   });
    
  922. 
    
  923.   // @gate enableLegacyCache
    
  924.   it('can resume rendering earlier than a timeout', async () => {
    
  925.     ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
    
  926.     await waitForAll([]);
    
  927. 
    
  928.     React.startTransition(() => {
    
  929.       ReactNoop.render(
    
  930.         <Suspense fallback={<Text text="Loading..." />}>
    
  931.           <AsyncText text="Async" />
    
  932.         </Suspense>,
    
  933.       );
    
  934.     });
    
  935.     await waitForAll(['Suspend! [Async]', 'Loading...']);
    
  936.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  937. 
    
  938.     // Resolve the promise
    
  939.     await resolveText('Async');
    
  940.     // We can now resume rendering
    
  941.     await waitForAll(['Async']);
    
  942.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Async" />);
    
  943.   });
    
  944. 
    
  945.   // @gate enableLegacyCache
    
  946.   it('starts working on an update even if its priority falls between two suspended levels', async () => {
    
  947.     function App(props) {
    
  948.       return (
    
  949.         <Suspense fallback={<Text text="Loading..." />}>
    
  950.           {props.text === 'C' || props.text === 'S' ? (
    
  951.             <Text text={props.text} />
    
  952.           ) : (
    
  953.             <AsyncText text={props.text} />
    
  954.           )}
    
  955.         </Suspense>
    
  956.       );
    
  957.     }
    
  958. 
    
  959.     // First mount without suspending. This ensures we already have content
    
  960.     // showing so that subsequent updates will suspend.
    
  961.     ReactNoop.render(<App text="S" />);
    
  962.     await waitForAll(['S']);
    
  963. 
    
  964.     // Schedule an update, and suspend for up to 5 seconds.
    
  965.     React.startTransition(() => ReactNoop.render(<App text="A" />));
    
  966.     // The update should suspend.
    
  967.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  968.     expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
    
  969. 
    
  970.     // Advance time until right before it expires.
    
  971.     await advanceTimers(4999);
    
  972.     ReactNoop.expire(4999);
    
  973.     await waitForAll([]);
    
  974.     expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
    
  975. 
    
  976.     // Schedule another low priority update.
    
  977.     React.startTransition(() => ReactNoop.render(<App text="B" />));
    
  978.     // This update should also suspend.
    
  979.     await waitForAll(['Suspend! [B]', 'Loading...']);
    
  980.     expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
    
  981. 
    
  982.     // Schedule a regular update. Its expiration time will fall between
    
  983.     // the expiration times of the previous two updates.
    
  984.     ReactNoop.render(<App text="C" />);
    
  985.     await waitForAll(['C']);
    
  986.     expect(ReactNoop).toMatchRenderedOutput(<span prop="C" />);
    
  987. 
    
  988.     // Flush the remaining work.
    
  989.     await resolveText('A');
    
  990.     await resolveText('B');
    
  991.     // Nothing else to render.
    
  992.     await waitForAll([]);
    
  993.     expect(ReactNoop).toMatchRenderedOutput(<span prop="C" />);
    
  994.   });
    
  995. 
    
  996.   // @gate enableLegacyCache
    
  997.   it('a suspended update that expires', async () => {
    
  998.     // Regression test. This test used to fall into an infinite loop.
    
  999.     function ExpensiveText({text}) {
    
  1000.       // This causes the update to expire.
    
  1001.       Scheduler.unstable_advanceTime(10000);
    
  1002.       // Then something suspends.
    
  1003.       return <AsyncText text={text} />;
    
  1004.     }
    
  1005. 
    
  1006.     function App() {
    
  1007.       return (
    
  1008.         <Suspense fallback="Loading...">
    
  1009.           <ExpensiveText text="A" />
    
  1010.           <ExpensiveText text="B" />
    
  1011.           <ExpensiveText text="C" />
    
  1012.         </Suspense>
    
  1013.       );
    
  1014.     }
    
  1015. 
    
  1016.     ReactNoop.render(<App />);
    
  1017.     await waitForAll(['Suspend! [A]']);
    
  1018.     expect(ReactNoop).toMatchRenderedOutput('Loading...');
    
  1019. 
    
  1020.     await resolveText('A');
    
  1021.     await resolveText('B');
    
  1022.     await resolveText('C');
    
  1023. 
    
  1024.     await waitForAll(['A', 'B', 'C']);
    
  1025.     expect(ReactNoop).toMatchRenderedOutput(
    
  1026.       <>
    
  1027.         <span prop="A" />
    
  1028.         <span prop="B" />
    
  1029.         <span prop="C" />
    
  1030.       </>,
    
  1031.     );
    
  1032.   });
    
  1033. 
    
  1034.   describe('legacy mode mode', () => {
    
  1035.     // @gate enableLegacyCache
    
  1036.     it('times out immediately', async () => {
    
  1037.       function App() {
    
  1038.         return (
    
  1039.           <Suspense fallback={<Text text="Loading..." />}>
    
  1040.             <AsyncText text="Result" />
    
  1041.           </Suspense>
    
  1042.         );
    
  1043.       }
    
  1044. 
    
  1045.       // Times out immediately, ignoring the specified threshold.
    
  1046.       ReactNoop.renderLegacySyncRoot(<App />);
    
  1047.       assertLog(['Suspend! [Result]', 'Loading...']);
    
  1048.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1049. 
    
  1050.       await act(() => {
    
  1051.         resolveText('Result');
    
  1052.       });
    
  1053. 
    
  1054.       assertLog(['Result']);
    
  1055.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Result" />);
    
  1056.     });
    
  1057. 
    
  1058.     // @gate enableLegacyCache
    
  1059.     it('times out immediately when Suspense is in legacy mode', async () => {
    
  1060.       class UpdatingText extends React.Component {
    
  1061.         state = {step: 1};
    
  1062.         render() {
    
  1063.           return <AsyncText text={`Step: ${this.state.step}`} />;
    
  1064.         }
    
  1065.       }
    
  1066. 
    
  1067.       function Spinner() {
    
  1068.         return (
    
  1069.           <Fragment>
    
  1070.             <Text text="Loading (1)" />
    
  1071.             <Text text="Loading (2)" />
    
  1072.             <Text text="Loading (3)" />
    
  1073.           </Fragment>
    
  1074.         );
    
  1075.       }
    
  1076. 
    
  1077.       const text = React.createRef(null);
    
  1078.       function App() {
    
  1079.         return (
    
  1080.           <Suspense fallback={<Spinner />}>
    
  1081.             <UpdatingText ref={text} />
    
  1082.             <Text text="Sibling" />
    
  1083.           </Suspense>
    
  1084.         );
    
  1085.       }
    
  1086. 
    
  1087.       // Initial mount.
    
  1088.       await seedNextTextCache('Step: 1');
    
  1089.       ReactNoop.renderLegacySyncRoot(<App />);
    
  1090.       assertLog(['Step: 1', 'Sibling']);
    
  1091.       expect(ReactNoop).toMatchRenderedOutput(
    
  1092.         <>
    
  1093.           <span prop="Step: 1" />
    
  1094.           <span prop="Sibling" />
    
  1095.         </>,
    
  1096.       );
    
  1097. 
    
  1098.       // Update.
    
  1099.       text.current.setState({step: 2}, () =>
    
  1100.         Scheduler.log('Update did commit'),
    
  1101.       );
    
  1102. 
    
  1103.       expect(ReactNoop.flushNextYield()).toEqual([
    
  1104.         'Suspend! [Step: 2]',
    
  1105.         'Loading (1)',
    
  1106.         'Loading (2)',
    
  1107.         'Loading (3)',
    
  1108.         'Update did commit',
    
  1109.       ]);
    
  1110.       expect(ReactNoop).toMatchRenderedOutput(
    
  1111.         <>
    
  1112.           <span hidden={true} prop="Step: 1" />
    
  1113.           <span hidden={true} prop="Sibling" />
    
  1114.           <span prop="Loading (1)" />
    
  1115.           <span prop="Loading (2)" />
    
  1116.           <span prop="Loading (3)" />
    
  1117.         </>,
    
  1118.       );
    
  1119. 
    
  1120.       await act(() => {
    
  1121.         resolveText('Step: 2');
    
  1122.       });
    
  1123.       assertLog(['Step: 2']);
    
  1124.       expect(ReactNoop).toMatchRenderedOutput(
    
  1125.         <>
    
  1126.           <span prop="Step: 2" />
    
  1127.           <span prop="Sibling" />
    
  1128.         </>,
    
  1129.       );
    
  1130.     });
    
  1131. 
    
  1132.     // @gate enableLegacyCache
    
  1133.     it('does not re-render siblings in loose mode', async () => {
    
  1134.       class TextWithLifecycle extends React.Component {
    
  1135.         componentDidMount() {
    
  1136.           Scheduler.log(`Mount [${this.props.text}]`);
    
  1137.         }
    
  1138.         componentDidUpdate() {
    
  1139.           Scheduler.log(`Update [${this.props.text}]`);
    
  1140.         }
    
  1141.         render() {
    
  1142.           return <Text {...this.props} />;
    
  1143.         }
    
  1144.       }
    
  1145. 
    
  1146.       class AsyncTextWithLifecycle extends React.Component {
    
  1147.         componentDidMount() {
    
  1148.           Scheduler.log(`Mount [${this.props.text}]`);
    
  1149.         }
    
  1150.         componentDidUpdate() {
    
  1151.           Scheduler.log(`Update [${this.props.text}]`);
    
  1152.         }
    
  1153.         render() {
    
  1154.           return <AsyncText {...this.props} />;
    
  1155.         }
    
  1156.       }
    
  1157. 
    
  1158.       function App() {
    
  1159.         return (
    
  1160.           <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
    
  1161.             <TextWithLifecycle text="A" />
    
  1162.             <AsyncTextWithLifecycle text="B" />
    
  1163.             <TextWithLifecycle text="C" />
    
  1164.           </Suspense>
    
  1165.         );
    
  1166.       }
    
  1167. 
    
  1168.       ReactNoop.renderLegacySyncRoot(<App />, () =>
    
  1169.         Scheduler.log('Commit root'),
    
  1170.       );
    
  1171.       assertLog([
    
  1172.         'A',
    
  1173.         'Suspend! [B]',
    
  1174.         'C',
    
  1175. 
    
  1176.         'Loading...',
    
  1177.         'Mount [A]',
    
  1178.         'Mount [B]',
    
  1179.         'Mount [C]',
    
  1180.         // This should be a mount, not an update.
    
  1181.         'Mount [Loading...]',
    
  1182.         'Commit root',
    
  1183.       ]);
    
  1184.       expect(ReactNoop).toMatchRenderedOutput(
    
  1185.         <>
    
  1186.           <span hidden={true} prop="A" />
    
  1187.           <span hidden={true} prop="C" />
    
  1188. 
    
  1189.           <span prop="Loading..." />
    
  1190.         </>,
    
  1191.       );
    
  1192. 
    
  1193.       await act(() => {
    
  1194.         resolveText('B');
    
  1195.       });
    
  1196. 
    
  1197.       assertLog(['B']);
    
  1198.       expect(ReactNoop).toMatchRenderedOutput(
    
  1199.         <>
    
  1200.           <span prop="A" />
    
  1201.           <span prop="B" />
    
  1202.           <span prop="C" />
    
  1203.         </>,
    
  1204.       );
    
  1205.     });
    
  1206. 
    
  1207.     // @gate enableLegacyCache
    
  1208.     it('suspends inside constructor', async () => {
    
  1209.       class AsyncTextInConstructor extends React.Component {
    
  1210.         constructor(props) {
    
  1211.           super(props);
    
  1212.           const text = props.text;
    
  1213.           Scheduler.log('constructor');
    
  1214.           readText(text);
    
  1215.           this.state = {text};
    
  1216.         }
    
  1217.         componentDidMount() {
    
  1218.           Scheduler.log('componentDidMount');
    
  1219.         }
    
  1220.         render() {
    
  1221.           Scheduler.log(this.state.text);
    
  1222.           return <span prop={this.state.text} />;
    
  1223.         }
    
  1224.       }
    
  1225. 
    
  1226.       ReactNoop.renderLegacySyncRoot(
    
  1227.         <Suspense fallback={<Text text="Loading..." />}>
    
  1228.           <AsyncTextInConstructor text="Hi" />
    
  1229.         </Suspense>,
    
  1230.       );
    
  1231. 
    
  1232.       assertLog(['constructor', 'Suspend! [Hi]', 'Loading...']);
    
  1233.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1234. 
    
  1235.       await act(() => {
    
  1236.         resolveText('Hi');
    
  1237.       });
    
  1238. 
    
  1239.       assertLog(['constructor', 'Hi', 'componentDidMount']);
    
  1240.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Hi" />);
    
  1241.     });
    
  1242. 
    
  1243.     // @gate enableLegacyCache
    
  1244.     it('does not infinite loop if fallback contains lifecycle method', async () => {
    
  1245.       class Fallback extends React.Component {
    
  1246.         state = {
    
  1247.           name: 'foo',
    
  1248.         };
    
  1249.         componentDidMount() {
    
  1250.           this.setState({
    
  1251.             name: 'bar',
    
  1252.           });
    
  1253.         }
    
  1254.         render() {
    
  1255.           return <Text text="Loading..." />;
    
  1256.         }
    
  1257.       }
    
  1258. 
    
  1259.       class Demo extends React.Component {
    
  1260.         render() {
    
  1261.           return (
    
  1262.             <Suspense fallback={<Fallback />}>
    
  1263.               <AsyncText text="Hi" />
    
  1264.             </Suspense>
    
  1265.           );
    
  1266.         }
    
  1267.       }
    
  1268. 
    
  1269.       ReactNoop.renderLegacySyncRoot(<Demo />);
    
  1270. 
    
  1271.       assertLog([
    
  1272.         'Suspend! [Hi]',
    
  1273.         'Loading...',
    
  1274.         // Re-render due to lifecycle update
    
  1275.         'Loading...',
    
  1276.       ]);
    
  1277.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1278.       await act(() => {
    
  1279.         resolveText('Hi');
    
  1280.       });
    
  1281.       assertLog(['Hi']);
    
  1282.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Hi" />);
    
  1283.     });
    
  1284. 
    
  1285.     if (global.__PERSISTENT__) {
    
  1286.       // @gate enableLegacyCache
    
  1287.       it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
    
  1288.         const {useRef, useLayoutEffect} = React;
    
  1289. 
    
  1290.         function Parent() {
    
  1291.           const child = useRef(null);
    
  1292. 
    
  1293.           useLayoutEffect(() => {
    
  1294.             Scheduler.log(ReactNoop.getPendingChildrenAsJSX());
    
  1295.           });
    
  1296. 
    
  1297.           return (
    
  1298.             <span ref={child} hidden={false}>
    
  1299.               <AsyncText text="Hi" />
    
  1300.             </span>
    
  1301.           );
    
  1302.         }
    
  1303. 
    
  1304.         function App(props) {
    
  1305.           return (
    
  1306.             <Suspense fallback={<Text text="Loading..." />}>
    
  1307.               <Parent />
    
  1308.             </Suspense>
    
  1309.           );
    
  1310.         }
    
  1311. 
    
  1312.         ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
    
  1313. 
    
  1314.         assertLog([
    
  1315.           'Suspend! [Hi]',
    
  1316.           'Loading...',
    
  1317.           // The child should have already been hidden
    
  1318.           <>
    
  1319.             <span hidden={true} />
    
  1320.             <span prop="Loading..." />
    
  1321.           </>,
    
  1322.         ]);
    
  1323. 
    
  1324.         await act(() => {
    
  1325.           resolveText('Hi');
    
  1326.         });
    
  1327.         assertLog(['Hi']);
    
  1328.       });
    
  1329.     } else {
    
  1330.       // @gate enableLegacyCache
    
  1331.       it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
    
  1332.         const {useRef, useLayoutEffect} = React;
    
  1333. 
    
  1334.         function Parent() {
    
  1335.           const child = useRef(null);
    
  1336. 
    
  1337.           useLayoutEffect(() => {
    
  1338.             Scheduler.log('Child is hidden: ' + child.current.hidden);
    
  1339.           });
    
  1340. 
    
  1341.           return (
    
  1342.             <span ref={child} hidden={false}>
    
  1343.               <AsyncText text="Hi" />
    
  1344.             </span>
    
  1345.           );
    
  1346.         }
    
  1347. 
    
  1348.         function App(props) {
    
  1349.           return (
    
  1350.             <Suspense fallback={<Text text="Loading..." />}>
    
  1351.               <Parent />
    
  1352.             </Suspense>
    
  1353.           );
    
  1354.         }
    
  1355. 
    
  1356.         ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
    
  1357. 
    
  1358.         assertLog([
    
  1359.           'Suspend! [Hi]',
    
  1360.           'Loading...',
    
  1361.           // The child should have already been hidden
    
  1362.           'Child is hidden: true',
    
  1363.         ]);
    
  1364. 
    
  1365.         await act(() => {
    
  1366.           resolveText('Hi');
    
  1367.         });
    
  1368. 
    
  1369.         assertLog(['Hi']);
    
  1370.       });
    
  1371.     }
    
  1372. 
    
  1373.     // @gate enableLegacyCache
    
  1374.     it('handles errors in the return path of a component that suspends', async () => {
    
  1375.       // Covers an edge case where an error is thrown inside the complete phase
    
  1376.       // of a component that is in the return path of a component that suspends.
    
  1377.       // The second error should also be handled (i.e. able to be captured by
    
  1378.       // an error boundary.
    
  1379.       class ErrorBoundary extends React.Component {
    
  1380.         state = {error: null};
    
  1381.         static getDerivedStateFromError(error, errorInfo) {
    
  1382.           return {error};
    
  1383.         }
    
  1384.         render() {
    
  1385.           if (this.state.error) {
    
  1386.             return `Caught an error: ${this.state.error.message}`;
    
  1387.           }
    
  1388.           return this.props.children;
    
  1389.         }
    
  1390.       }
    
  1391. 
    
  1392.       ReactNoop.renderLegacySyncRoot(
    
  1393.         <ErrorBoundary>
    
  1394.           <Suspense fallback="Loading...">
    
  1395.             <errorInCompletePhase>
    
  1396.               <AsyncText text="Async" />
    
  1397.             </errorInCompletePhase>
    
  1398.           </Suspense>
    
  1399.         </ErrorBoundary>,
    
  1400.       );
    
  1401. 
    
  1402.       assertLog(['Suspend! [Async]']);
    
  1403.       expect(ReactNoop).toMatchRenderedOutput(
    
  1404.         'Caught an error: Error in host config.',
    
  1405.       );
    
  1406.     });
    
  1407. 
    
  1408.     it('does not drop mounted effects', async () => {
    
  1409.       const never = {then() {}};
    
  1410. 
    
  1411.       let setShouldSuspend;
    
  1412.       function App() {
    
  1413.         const [shouldSuspend, _setShouldSuspend] = React.useState(0);
    
  1414.         setShouldSuspend = _setShouldSuspend;
    
  1415.         return (
    
  1416.           <Suspense fallback="Loading...">
    
  1417.             <Child shouldSuspend={shouldSuspend} />
    
  1418.           </Suspense>
    
  1419.         );
    
  1420.       }
    
  1421. 
    
  1422.       function Child({shouldSuspend}) {
    
  1423.         if (shouldSuspend) {
    
  1424.           throw never;
    
  1425.         }
    
  1426. 
    
  1427.         React.useEffect(() => {
    
  1428.           Scheduler.log('Mount');
    
  1429.           return () => {
    
  1430.             Scheduler.log('Unmount');
    
  1431.           };
    
  1432.         }, []);
    
  1433. 
    
  1434.         return 'Child';
    
  1435.       }
    
  1436. 
    
  1437.       const root = ReactNoop.createLegacyRoot(null);
    
  1438.       await act(() => {
    
  1439.         root.render(<App />);
    
  1440.       });
    
  1441.       assertLog(['Mount']);
    
  1442.       expect(root).toMatchRenderedOutput('Child');
    
  1443. 
    
  1444.       // Suspend the child. This puts it into an inconsistent state.
    
  1445.       await act(() => {
    
  1446.         setShouldSuspend(true);
    
  1447.       });
    
  1448.       expect(root).toMatchRenderedOutput('Loading...');
    
  1449. 
    
  1450.       // Unmount everything
    
  1451.       await act(() => {
    
  1452.         root.render(null);
    
  1453.       });
    
  1454.       assertLog(['Unmount']);
    
  1455.     });
    
  1456.   });
    
  1457. 
    
  1458.   // @gate enableLegacyCache
    
  1459.   it('does not call lifecycles of a suspended component', async () => {
    
  1460.     class TextWithLifecycle extends React.Component {
    
  1461.       componentDidMount() {
    
  1462.         Scheduler.log(`Mount [${this.props.text}]`);
    
  1463.       }
    
  1464.       componentDidUpdate() {
    
  1465.         Scheduler.log(`Update [${this.props.text}]`);
    
  1466.       }
    
  1467.       componentWillUnmount() {
    
  1468.         Scheduler.log(`Unmount [${this.props.text}]`);
    
  1469.       }
    
  1470.       render() {
    
  1471.         return <Text {...this.props} />;
    
  1472.       }
    
  1473.     }
    
  1474. 
    
  1475.     class AsyncTextWithLifecycle extends React.Component {
    
  1476.       componentDidMount() {
    
  1477.         Scheduler.log(`Mount [${this.props.text}]`);
    
  1478.       }
    
  1479.       componentDidUpdate() {
    
  1480.         Scheduler.log(`Update [${this.props.text}]`);
    
  1481.       }
    
  1482.       componentWillUnmount() {
    
  1483.         Scheduler.log(`Unmount [${this.props.text}]`);
    
  1484.       }
    
  1485.       render() {
    
  1486.         const text = this.props.text;
    
  1487.         readText(text);
    
  1488.         Scheduler.log(text);
    
  1489.         return <span prop={text} />;
    
  1490.       }
    
  1491.     }
    
  1492. 
    
  1493.     function App() {
    
  1494.       return (
    
  1495.         <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
    
  1496.           <TextWithLifecycle text="A" />
    
  1497.           <AsyncTextWithLifecycle text="B" />
    
  1498.           <TextWithLifecycle text="C" />
    
  1499.         </Suspense>
    
  1500.       );
    
  1501.     }
    
  1502. 
    
  1503.     ReactNoop.renderLegacySyncRoot(<App />, () => Scheduler.log('Commit root'));
    
  1504.     assertLog([
    
  1505.       'A',
    
  1506.       'Suspend! [B]',
    
  1507.       'C',
    
  1508.       'Loading...',
    
  1509. 
    
  1510.       'Mount [A]',
    
  1511.       // B's lifecycle should not fire because it suspended
    
  1512.       // 'Mount [B]',
    
  1513.       'Mount [C]',
    
  1514.       'Mount [Loading...]',
    
  1515.       'Commit root',
    
  1516.     ]);
    
  1517.     expect(ReactNoop).toMatchRenderedOutput(
    
  1518.       <>
    
  1519.         <span hidden={true} prop="A" />
    
  1520.         <span hidden={true} prop="C" />
    
  1521.         <span prop="Loading..." />
    
  1522.       </>,
    
  1523.     );
    
  1524.   });
    
  1525. 
    
  1526.   // @gate enableLegacyCache
    
  1527.   it('does not call lifecycles of a suspended component (hooks)', async () => {
    
  1528.     function TextWithLifecycle(props) {
    
  1529.       React.useLayoutEffect(() => {
    
  1530.         Scheduler.log(`Layout Effect [${props.text}]`);
    
  1531.         return () => {
    
  1532.           Scheduler.log(`Destroy Layout Effect [${props.text}]`);
    
  1533.         };
    
  1534.       }, [props.text]);
    
  1535.       React.useEffect(() => {
    
  1536.         Scheduler.log(`Effect [${props.text}]`);
    
  1537.         return () => {
    
  1538.           Scheduler.log(`Destroy Effect [${props.text}]`);
    
  1539.         };
    
  1540.       }, [props.text]);
    
  1541.       return <Text {...props} />;
    
  1542.     }
    
  1543. 
    
  1544.     function AsyncTextWithLifecycle(props) {
    
  1545.       React.useLayoutEffect(() => {
    
  1546.         Scheduler.log(`Layout Effect [${props.text}]`);
    
  1547.         return () => {
    
  1548.           Scheduler.log(`Destroy Layout Effect [${props.text}]`);
    
  1549.         };
    
  1550.       }, [props.text]);
    
  1551.       React.useEffect(() => {
    
  1552.         Scheduler.log(`Effect [${props.text}]`);
    
  1553.         return () => {
    
  1554.           Scheduler.log(`Destroy Effect [${props.text}]`);
    
  1555.         };
    
  1556.       }, [props.text]);
    
  1557.       const text = props.text;
    
  1558.       readText(text);
    
  1559.       Scheduler.log(text);
    
  1560.       return <span prop={text} />;
    
  1561.     }
    
  1562. 
    
  1563.     function App({text}) {
    
  1564.       return (
    
  1565.         <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
    
  1566.           <TextWithLifecycle text="A" />
    
  1567.           <AsyncTextWithLifecycle text={text} />
    
  1568.           <TextWithLifecycle text="C" />
    
  1569.         </Suspense>
    
  1570.       );
    
  1571.     }
    
  1572. 
    
  1573.     ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
    
  1574.       Scheduler.log('Commit root'),
    
  1575.     );
    
  1576.     assertLog([
    
  1577.       'A',
    
  1578.       'Suspend! [B]',
    
  1579.       'C',
    
  1580.       'Loading...',
    
  1581. 
    
  1582.       'Layout Effect [A]',
    
  1583.       // B's effect should not fire because it suspended
    
  1584.       // 'Layout Effect [B]',
    
  1585.       'Layout Effect [C]',
    
  1586.       'Layout Effect [Loading...]',
    
  1587.       'Commit root',
    
  1588.     ]);
    
  1589. 
    
  1590.     // Flush passive effects.
    
  1591.     await waitForAll([
    
  1592.       'Effect [A]',
    
  1593.       // B's effect should not fire because it suspended
    
  1594.       // 'Effect [B]',
    
  1595.       'Effect [C]',
    
  1596.       'Effect [Loading...]',
    
  1597.     ]);
    
  1598. 
    
  1599.     expect(ReactNoop).toMatchRenderedOutput(
    
  1600.       <>
    
  1601.         <span hidden={true} prop="A" />
    
  1602.         <span hidden={true} prop="C" />
    
  1603.         <span prop="Loading..." />
    
  1604.       </>,
    
  1605.     );
    
  1606. 
    
  1607.     await act(() => {
    
  1608.       resolveText('B');
    
  1609.     });
    
  1610. 
    
  1611.     assertLog([
    
  1612.       'B',
    
  1613.       'Destroy Layout Effect [Loading...]',
    
  1614.       'Layout Effect [B]',
    
  1615.       'Destroy Effect [Loading...]',
    
  1616.       'Effect [B]',
    
  1617.     ]);
    
  1618. 
    
  1619.     // Update
    
  1620.     ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
    
  1621.       Scheduler.log('Commit root'),
    
  1622.     );
    
  1623. 
    
  1624.     assertLog([
    
  1625.       'A',
    
  1626.       'Suspend! [B2]',
    
  1627.       'C',
    
  1628.       'Loading...',
    
  1629. 
    
  1630.       // B2's effect should not fire because it suspended
    
  1631.       // 'Layout Effect [B2]',
    
  1632.       'Layout Effect [Loading...]',
    
  1633.       'Commit root',
    
  1634.     ]);
    
  1635. 
    
  1636.     // Flush passive effects.
    
  1637.     await waitForAll([
    
  1638.       // B2's effect should not fire because it suspended
    
  1639.       // 'Effect [B2]',
    
  1640.       'Effect [Loading...]',
    
  1641.     ]);
    
  1642. 
    
  1643.     await act(() => {
    
  1644.       resolveText('B2');
    
  1645.     });
    
  1646. 
    
  1647.     assertLog([
    
  1648.       'B2',
    
  1649.       'Destroy Layout Effect [Loading...]',
    
  1650.       'Destroy Layout Effect [B]',
    
  1651.       'Layout Effect [B2]',
    
  1652.       'Destroy Effect [Loading...]',
    
  1653.       'Destroy Effect [B]',
    
  1654.       'Effect [B2]',
    
  1655.     ]);
    
  1656.   });
    
  1657. 
    
  1658.   // @gate enableLegacyCache
    
  1659.   it('does not suspends if a fallback has been shown for a long time', async () => {
    
  1660.     function Foo() {
    
  1661.       Scheduler.log('Foo');
    
  1662.       return (
    
  1663.         <Suspense fallback={<Text text="Loading..." />}>
    
  1664.           <AsyncText text="A" />
    
  1665.           <Suspense fallback={<Text text="Loading more..." />}>
    
  1666.             <AsyncText text="B" />
    
  1667.           </Suspense>
    
  1668.         </Suspense>
    
  1669.       );
    
  1670.     }
    
  1671. 
    
  1672.     ReactNoop.render(<Foo />);
    
  1673.     // Start rendering
    
  1674.     await waitForAll([
    
  1675.       'Foo',
    
  1676.       // A suspends
    
  1677.       'Suspend! [A]',
    
  1678.       'Loading...',
    
  1679.     ]);
    
  1680.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1681. 
    
  1682.     // Wait a long time.
    
  1683.     Scheduler.unstable_advanceTime(5000);
    
  1684.     await advanceTimers(5000);
    
  1685. 
    
  1686.     // Retry with the new content.
    
  1687.     await resolveText('A');
    
  1688.     await waitForAll([
    
  1689.       'A',
    
  1690.       // B suspends
    
  1691.       'Suspend! [B]',
    
  1692.       'Loading more...',
    
  1693.     ]);
    
  1694. 
    
  1695.     // Because we've already been waiting for so long we've exceeded
    
  1696.     // our threshold and we show the next level immediately.
    
  1697.     expect(ReactNoop).toMatchRenderedOutput(
    
  1698.       <>
    
  1699.         <span prop="A" />
    
  1700.         <span prop="Loading more..." />
    
  1701.       </>,
    
  1702.     );
    
  1703. 
    
  1704.     // Flush the last promise completely
    
  1705.     await act(() => resolveText('B'));
    
  1706.     // Renders successfully
    
  1707.     assertLog(['B']);
    
  1708.     expect(ReactNoop).toMatchRenderedOutput(
    
  1709.       <>
    
  1710.         <span prop="A" />
    
  1711.         <span prop="B" />
    
  1712.       </>,
    
  1713.     );
    
  1714.   });
    
  1715. 
    
  1716.   // @gate enableLegacyCache
    
  1717.   it('throttles content from appearing if a fallback was shown recently', async () => {
    
  1718.     function Foo() {
    
  1719.       Scheduler.log('Foo');
    
  1720.       return (
    
  1721.         <Suspense fallback={<Text text="Loading..." />}>
    
  1722.           <AsyncText text="A" />
    
  1723.           <Suspense fallback={<Text text="Loading more..." />}>
    
  1724.             <AsyncText text="B" />
    
  1725.           </Suspense>
    
  1726.         </Suspense>
    
  1727.       );
    
  1728.     }
    
  1729. 
    
  1730.     ReactNoop.render(<Foo />);
    
  1731.     // Start rendering
    
  1732.     await waitForAll([
    
  1733.       'Foo',
    
  1734.       // A suspends
    
  1735.       'Suspend! [A]',
    
  1736.       'Loading...',
    
  1737.     ]);
    
  1738.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1739. 
    
  1740.     await act(async () => {
    
  1741.       await resolveText('A');
    
  1742. 
    
  1743.       // Retry with the new content.
    
  1744.       await waitForAll([
    
  1745.         'A',
    
  1746.         // B suspends
    
  1747.         'Suspend! [B]',
    
  1748.         'Loading more...',
    
  1749.       ]);
    
  1750.       // Because we've already been waiting for so long we can
    
  1751.       // wait a bit longer. Still nothing...
    
  1752.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1753. 
    
  1754.       // Before we commit another Promise resolves.
    
  1755.       // We're still showing the first loading state.
    
  1756.       await resolveText('B');
    
  1757.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1758. 
    
  1759.       // Restart and render the complete content.
    
  1760.       await waitForAll(['A', 'B']);
    
  1761. 
    
  1762.       if (gate(flags => flags.alwaysThrottleRetries)) {
    
  1763.         // Correct behavior:
    
  1764.         //
    
  1765.         // The tree will finish but we won't commit the result yet because the fallback appeared recently.
    
  1766.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  1767.       } else {
    
  1768.         // Old behavior, gated until this rolls out at Meta:
    
  1769.         //
    
  1770.         // TODO: Because this render was the result of a retry, and a fallback
    
  1771.         // was shown recently, we should suspend and remain on the fallback for
    
  1772.         // little bit longer. We currently only do this if there's still
    
  1773.         // remaining fallbacks in the tree, but we should do it for all retries.
    
  1774.         expect(ReactNoop).toMatchRenderedOutput(
    
  1775.           <>
    
  1776.             <span prop="A" />
    
  1777.             <span prop="B" />
    
  1778.           </>,
    
  1779.         );
    
  1780.       }
    
  1781.     });
    
  1782.     assertLog([]);
    
  1783.     expect(ReactNoop).toMatchRenderedOutput(
    
  1784.       <>
    
  1785.         <span prop="A" />
    
  1786.         <span prop="B" />
    
  1787.       </>,
    
  1788.     );
    
  1789.   });
    
  1790. 
    
  1791.   // @gate enableLegacyCache
    
  1792.   it('throttles content from appearing if a fallback was filled in recently', async () => {
    
  1793.     function Foo() {
    
  1794.       Scheduler.log('Foo');
    
  1795.       return (
    
  1796.         <>
    
  1797.           <Suspense fallback={<Text text="Loading A..." />}>
    
  1798.             <AsyncText text="A" />
    
  1799.           </Suspense>
    
  1800.           <Suspense fallback={<Text text="Loading B..." />}>
    
  1801.             <AsyncText text="B" />
    
  1802.           </Suspense>
    
  1803.         </>
    
  1804.       );
    
  1805.     }
    
  1806. 
    
  1807.     ReactNoop.render(<Foo />);
    
  1808.     // Start rendering
    
  1809.     await waitForAll([
    
  1810.       'Foo',
    
  1811.       'Suspend! [A]',
    
  1812.       'Loading A...',
    
  1813.       'Suspend! [B]',
    
  1814.       'Loading B...',
    
  1815.     ]);
    
  1816.     expect(ReactNoop).toMatchRenderedOutput(
    
  1817.       <>
    
  1818.         <span prop="Loading A..." />
    
  1819.         <span prop="Loading B..." />
    
  1820.       </>,
    
  1821.     );
    
  1822. 
    
  1823.     // Resolve only A. B will still be loading.
    
  1824.     await act(async () => {
    
  1825.       await resolveText('A');
    
  1826. 
    
  1827.       // If we didn't advance the time here, A would not commit; it would
    
  1828.       // be throttled because the fallback would have appeared too recently.
    
  1829.       Scheduler.unstable_advanceTime(10000);
    
  1830.       jest.advanceTimersByTime(10000);
    
  1831.       await waitForPaint(['A']);
    
  1832.       expect(ReactNoop).toMatchRenderedOutput(
    
  1833.         <>
    
  1834.           <span prop="A" />
    
  1835.           <span prop="Loading B..." />
    
  1836.         </>,
    
  1837.       );
    
  1838.     });
    
  1839. 
    
  1840.     // Advance by a small amount of time. For testing purposes, this is meant
    
  1841.     // to be just under the throttling interval. It's a heurstic, though, so
    
  1842.     // if we adjust the heuristic we might have to update this test, too.
    
  1843.     Scheduler.unstable_advanceTime(200);
    
  1844.     jest.advanceTimersByTime(200);
    
  1845. 
    
  1846.     // Now resolve B.
    
  1847.     await act(async () => {
    
  1848.       await resolveText('B');
    
  1849.       await waitForPaint(['B']);
    
  1850. 
    
  1851.       if (gate(flags => flags.alwaysThrottleRetries)) {
    
  1852.         // B should not commit yet. Even though it's been a long time since its
    
  1853.         // fallback was shown, it hasn't been long since A appeared. So B's
    
  1854.         // appearance is throttled to reduce jank.
    
  1855.         expect(ReactNoop).toMatchRenderedOutput(
    
  1856.           <>
    
  1857.             <span prop="A" />
    
  1858.             <span prop="Loading B..." />
    
  1859.           </>,
    
  1860.         );
    
  1861. 
    
  1862.         // Advance time a little bit more. Now it commits because enough time
    
  1863.         // has passed.
    
  1864.         Scheduler.unstable_advanceTime(100);
    
  1865.         jest.advanceTimersByTime(100);
    
  1866.         await waitForAll([]);
    
  1867.         expect(ReactNoop).toMatchRenderedOutput(
    
  1868.           <>
    
  1869.             <span prop="A" />
    
  1870.             <span prop="B" />
    
  1871.           </>,
    
  1872.         );
    
  1873.       } else {
    
  1874.         // Old behavior, gated until this rolls out at Meta:
    
  1875.         //
    
  1876.         // B appears immediately, without being throttled.
    
  1877.         expect(ReactNoop).toMatchRenderedOutput(
    
  1878.           <>
    
  1879.             <span prop="A" />
    
  1880.             <span prop="B" />
    
  1881.           </>,
    
  1882.         );
    
  1883.       }
    
  1884.     });
    
  1885.   });
    
  1886. 
    
  1887.   // TODO: flip to "warns" when this is implemented again.
    
  1888.   // @gate enableLegacyCache
    
  1889.   it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
    
  1890.     let _setShow;
    
  1891.     function App() {
    
  1892.       const [show, setShow] = React.useState(false);
    
  1893.       _setShow = setShow;
    
  1894.       return (
    
  1895.         <Suspense fallback="Loading...">
    
  1896.           {show && <AsyncText text="A" />}
    
  1897.         </Suspense>
    
  1898.       );
    
  1899.     }
    
  1900. 
    
  1901.     await act(() => {
    
  1902.       ReactNoop.render(<App />);
    
  1903.     });
    
  1904. 
    
  1905.     // TODO: assert toErrorDev() when the warning is implemented again.
    
  1906.     await act(() => {
    
  1907.       ReactNoop.flushSync(() => _setShow(true));
    
  1908.     });
    
  1909.   });
    
  1910. 
    
  1911.   // TODO: flip to "warns" when this is implemented again.
    
  1912.   // @gate enableLegacyCache
    
  1913.   it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
    
  1914.     let show;
    
  1915.     class App extends React.Component {
    
  1916.       state = {show: false};
    
  1917. 
    
  1918.       render() {
    
  1919.         show = () => this.setState({show: true});
    
  1920.         return (
    
  1921.           <Suspense fallback="Loading...">
    
  1922.             {this.state.show && <AsyncText text="A" />}
    
  1923.           </Suspense>
    
  1924.         );
    
  1925.       }
    
  1926.     }
    
  1927. 
    
  1928.     await act(() => {
    
  1929.       ReactNoop.render(<App />);
    
  1930.     });
    
  1931. 
    
  1932.     // TODO: assert toErrorDev() when the warning is implemented again.
    
  1933.     await act(() => {
    
  1934.       ReactNoop.flushSync(() => show());
    
  1935.     });
    
  1936.   });
    
  1937. 
    
  1938.   // @gate enableLegacyCache
    
  1939.   it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
    
  1940.     let showB;
    
  1941.     class App extends React.Component {
    
  1942.       state = {showB: false};
    
  1943. 
    
  1944.       render() {
    
  1945.         showB = () => this.setState({showB: true});
    
  1946.         return (
    
  1947.           <Suspense fallback="Loading...">
    
  1948.             {<AsyncText text="A" />}
    
  1949.             {this.state.showB && <AsyncText text="B" />}
    
  1950.           </Suspense>
    
  1951.         );
    
  1952.       }
    
  1953.     }
    
  1954. 
    
  1955.     await act(() => {
    
  1956.       ReactNoop.render(<App />);
    
  1957.     });
    
  1958. 
    
  1959.     assertLog(['Suspend! [A]']);
    
  1960.     expect(ReactNoop).toMatchRenderedOutput('Loading...');
    
  1961. 
    
  1962.     await act(() => {
    
  1963.       ReactNoop.flushSync(() => showB());
    
  1964.     });
    
  1965. 
    
  1966.     assertLog(['Suspend! [A]']);
    
  1967.   });
    
  1968. 
    
  1969.   // TODO: flip to "warns" when this is implemented again.
    
  1970.   // @gate enableLegacyCache
    
  1971.   it(
    
  1972.     'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
    
  1973.       'and component that suspended',
    
  1974.     async () => {
    
  1975.       let _setShow;
    
  1976.       function A() {
    
  1977.         const [show, setShow] = React.useState(false);
    
  1978.         _setShow = setShow;
    
  1979.         return show && <AsyncText text="A" />;
    
  1980.       }
    
  1981.       function App() {
    
  1982.         return (
    
  1983.           <Suspense fallback="Loading...">
    
  1984.             <A />
    
  1985.           </Suspense>
    
  1986.         );
    
  1987.       }
    
  1988.       await act(() => {
    
  1989.         ReactNoop.render(<App />);
    
  1990.       });
    
  1991. 
    
  1992.       // TODO: assert toErrorDev() when the warning is implemented again.
    
  1993.       await act(() => {
    
  1994.         ReactNoop.flushSync(() => _setShow(true));
    
  1995.       });
    
  1996.     },
    
  1997.   );
    
  1998. 
    
  1999.   // @gate enableLegacyCache
    
  2000.   it('normal priority updates suspending do not warn for class components', async () => {
    
  2001.     let show;
    
  2002.     class App extends React.Component {
    
  2003.       state = {show: false};
    
  2004. 
    
  2005.       render() {
    
  2006.         show = () => this.setState({show: true});
    
  2007.         return (
    
  2008.           <Suspense fallback="Loading...">
    
  2009.             {this.state.show && <AsyncText text="A" />}
    
  2010.           </Suspense>
    
  2011.         );
    
  2012.       }
    
  2013.     }
    
  2014. 
    
  2015.     await act(() => {
    
  2016.       ReactNoop.render(<App />);
    
  2017.     });
    
  2018. 
    
  2019.     // also make sure lowpriority is okay
    
  2020.     await act(() => show(true));
    
  2021. 
    
  2022.     assertLog(['Suspend! [A]']);
    
  2023.     await resolveText('A');
    
  2024. 
    
  2025.     expect(ReactNoop).toMatchRenderedOutput('Loading...');
    
  2026.   });
    
  2027. 
    
  2028.   // @gate enableLegacyCache
    
  2029.   it('normal priority updates suspending do not warn for functional components', async () => {
    
  2030.     let _setShow;
    
  2031.     function App() {
    
  2032.       const [show, setShow] = React.useState(false);
    
  2033.       _setShow = setShow;
    
  2034.       return (
    
  2035.         <Suspense fallback="Loading...">
    
  2036.           {show && <AsyncText text="A" />}
    
  2037.         </Suspense>
    
  2038.       );
    
  2039.     }
    
  2040. 
    
  2041.     await act(() => {
    
  2042.       ReactNoop.render(<App />);
    
  2043.     });
    
  2044. 
    
  2045.     // also make sure lowpriority is okay
    
  2046.     await act(() => _setShow(true));
    
  2047. 
    
  2048.     assertLog(['Suspend! [A]']);
    
  2049.     await resolveText('A');
    
  2050. 
    
  2051.     expect(ReactNoop).toMatchRenderedOutput('Loading...');
    
  2052.   });
    
  2053. 
    
  2054.   // @gate enableLegacyCache && enableSuspenseAvoidThisFallback
    
  2055.   it('shows the parent fallback if the inner fallback should be avoided', async () => {
    
  2056.     function Foo({showC}) {
    
  2057.       Scheduler.log('Foo');
    
  2058.       return (
    
  2059.         <Suspense fallback={<Text text="Initial load..." />}>
    
  2060.           <Suspense
    
  2061.             unstable_avoidThisFallback={true}
    
  2062.             fallback={<Text text="Updating..." />}>
    
  2063.             <AsyncText text="A" />
    
  2064.             {showC ? <AsyncText text="C" /> : null}
    
  2065.           </Suspense>
    
  2066.           <Text text="B" />
    
  2067.         </Suspense>
    
  2068.       );
    
  2069.     }
    
  2070. 
    
  2071.     ReactNoop.render(<Foo />);
    
  2072.     await waitForAll(['Foo', 'Suspend! [A]', 'Initial load...']);
    
  2073.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Initial load..." />);
    
  2074. 
    
  2075.     // Eventually we resolve and show the data.
    
  2076.     await act(() => resolveText('A'));
    
  2077.     assertLog(['A', 'B']);
    
  2078.     expect(ReactNoop).toMatchRenderedOutput(
    
  2079.       <>
    
  2080.         <span prop="A" />
    
  2081.         <span prop="B" />
    
  2082.       </>,
    
  2083.     );
    
  2084. 
    
  2085.     // Update to show C
    
  2086.     ReactNoop.render(<Foo showC={true} />);
    
  2087.     await waitForAll(['Foo', 'A', 'Suspend! [C]', 'Updating...', 'B']);
    
  2088.     // Flush to skip suspended time.
    
  2089.     Scheduler.unstable_advanceTime(600);
    
  2090.     await advanceTimers(600);
    
  2091.     // Since the optional suspense boundary is already showing its content,
    
  2092.     // we have to use the inner fallback instead.
    
  2093.     expect(ReactNoop).toMatchRenderedOutput(
    
  2094.       <>
    
  2095.         <span prop="A" hidden={true} />
    
  2096.         <span prop="Updating..." />
    
  2097.         <span prop="B" />
    
  2098.       </>,
    
  2099.     );
    
  2100. 
    
  2101.     // Later we load the data.
    
  2102.     await act(() => resolveText('C'));
    
  2103.     assertLog(['A', 'C']);
    
  2104.     expect(ReactNoop).toMatchRenderedOutput(
    
  2105.       <>
    
  2106.         <span prop="A" />
    
  2107.         <span prop="C" />
    
  2108.         <span prop="B" />
    
  2109.       </>,
    
  2110.     );
    
  2111.   });
    
  2112. 
    
  2113.   // @gate enableLegacyCache
    
  2114.   it('does not show the parent fallback if the inner fallback is not defined', async () => {
    
  2115.     function Foo({showC}) {
    
  2116.       Scheduler.log('Foo');
    
  2117.       return (
    
  2118.         <Suspense fallback={<Text text="Initial load..." />}>
    
  2119.           <Suspense>
    
  2120.             <AsyncText text="A" />
    
  2121.             {showC ? <AsyncText text="C" /> : null}
    
  2122.           </Suspense>
    
  2123.           <Text text="B" />
    
  2124.         </Suspense>
    
  2125.       );
    
  2126.     }
    
  2127. 
    
  2128.     ReactNoop.render(<Foo />);
    
  2129.     await waitForAll([
    
  2130.       'Foo',
    
  2131.       'Suspend! [A]',
    
  2132.       'B',
    
  2133.       // null
    
  2134.     ]);
    
  2135.     expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2136. 
    
  2137.     // Eventually we resolve and show the data.
    
  2138.     await act(() => resolveText('A'));
    
  2139.     assertLog(['A']);
    
  2140.     expect(ReactNoop).toMatchRenderedOutput(
    
  2141.       <>
    
  2142.         <span prop="A" />
    
  2143.         <span prop="B" />
    
  2144.       </>,
    
  2145.     );
    
  2146. 
    
  2147.     // Update to show C
    
  2148.     ReactNoop.render(<Foo showC={true} />);
    
  2149.     await waitForAll([
    
  2150.       'Foo',
    
  2151.       'A',
    
  2152.       'Suspend! [C]',
    
  2153.       // null
    
  2154.       'B',
    
  2155.     ]);
    
  2156.     // Flush to skip suspended time.
    
  2157.     Scheduler.unstable_advanceTime(600);
    
  2158.     await advanceTimers(600);
    
  2159.     expect(ReactNoop).toMatchRenderedOutput(
    
  2160.       <>
    
  2161.         <span prop="A" hidden={true} />
    
  2162.         <span prop="B" />
    
  2163.       </>,
    
  2164.     );
    
  2165. 
    
  2166.     // Later we load the data.
    
  2167.     await act(() => resolveText('C'));
    
  2168.     assertLog(['A', 'C']);
    
  2169.     expect(ReactNoop).toMatchRenderedOutput(
    
  2170.       <>
    
  2171.         <span prop="A" />
    
  2172.         <span prop="C" />
    
  2173.         <span prop="B" />
    
  2174.       </>,
    
  2175.     );
    
  2176.   });
    
  2177. 
    
  2178.   // @gate enableLegacyCache
    
  2179.   it('favors showing the inner fallback for nested top level avoided fallback', async () => {
    
  2180.     function Foo({showB}) {
    
  2181.       Scheduler.log('Foo');
    
  2182.       return (
    
  2183.         <Suspense
    
  2184.           unstable_avoidThisFallback={true}
    
  2185.           fallback={<Text text="Loading A..." />}>
    
  2186.           <Text text="A" />
    
  2187.           <Suspense
    
  2188.             unstable_avoidThisFallback={true}
    
  2189.             fallback={<Text text="Loading B..." />}>
    
  2190.             <AsyncText text="B" />
    
  2191.           </Suspense>
    
  2192.         </Suspense>
    
  2193.       );
    
  2194.     }
    
  2195. 
    
  2196.     ReactNoop.render(<Foo />);
    
  2197.     await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
    
  2198.     // Flush to skip suspended time.
    
  2199.     Scheduler.unstable_advanceTime(600);
    
  2200.     await advanceTimers(600);
    
  2201. 
    
  2202.     expect(ReactNoop).toMatchRenderedOutput(
    
  2203.       <>
    
  2204.         <span prop="A" />
    
  2205.         <span prop="Loading B..." />
    
  2206.       </>,
    
  2207.     );
    
  2208.   });
    
  2209. 
    
  2210.   // @gate enableLegacyCache && enableSuspenseAvoidThisFallback
    
  2211.   it('keeps showing an avoided parent fallback if it is already showing', async () => {
    
  2212.     function Foo({showB}) {
    
  2213.       Scheduler.log('Foo');
    
  2214.       return (
    
  2215.         <Suspense fallback={<Text text="Initial load..." />}>
    
  2216.           <Suspense
    
  2217.             unstable_avoidThisFallback={true}
    
  2218.             fallback={<Text text="Loading A..." />}>
    
  2219.             <Text text="A" />
    
  2220.             {showB ? (
    
  2221.               <Suspense
    
  2222.                 unstable_avoidThisFallback={true}
    
  2223.                 fallback={<Text text="Loading B..." />}>
    
  2224.                 <AsyncText text="B" />
    
  2225.               </Suspense>
    
  2226.             ) : null}
    
  2227.           </Suspense>
    
  2228.         </Suspense>
    
  2229.       );
    
  2230.     }
    
  2231. 
    
  2232.     ReactNoop.render(<Foo />);
    
  2233.     await waitForAll(['Foo', 'A']);
    
  2234.     expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2235. 
    
  2236.     if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  2237.       ReactNoop.render(<Foo showB={true} />);
    
  2238.     } else {
    
  2239.       React.startTransition(() => {
    
  2240.         ReactNoop.render(<Foo showB={true} />);
    
  2241.       });
    
  2242.     }
    
  2243. 
    
  2244.     await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
    
  2245. 
    
  2246.     if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  2247.       expect(ReactNoop).toMatchRenderedOutput(
    
  2248.         <>
    
  2249.           <span prop="A" />
    
  2250.           <span prop="Loading B..." />
    
  2251.         </>,
    
  2252.       );
    
  2253.     } else {
    
  2254.       // Transitions never fall back.
    
  2255.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2256.     }
    
  2257.   });
    
  2258. 
    
  2259.   // @gate enableLegacyCache
    
  2260.   it('keeps showing an undefined fallback if it is already showing', async () => {
    
  2261.     function Foo({showB}) {
    
  2262.       Scheduler.log('Foo');
    
  2263.       return (
    
  2264.         <Suspense fallback={<Text text="Initial load..." />}>
    
  2265.           <Suspense fallback={undefined}>
    
  2266.             <Text text="A" />
    
  2267.             {showB ? (
    
  2268.               <Suspense fallback={undefined}>
    
  2269.                 <AsyncText text="B" />
    
  2270.               </Suspense>
    
  2271.             ) : null}
    
  2272.           </Suspense>
    
  2273.         </Suspense>
    
  2274.       );
    
  2275.     }
    
  2276. 
    
  2277.     ReactNoop.render(<Foo />);
    
  2278.     await waitForAll(['Foo', 'A']);
    
  2279.     expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2280. 
    
  2281.     React.startTransition(() => {
    
  2282.       ReactNoop.render(<Foo showB={true} />);
    
  2283.     });
    
  2284. 
    
  2285.     await waitForAll([
    
  2286.       'Foo',
    
  2287.       'A',
    
  2288.       'Suspend! [B]',
    
  2289.       // Null
    
  2290.     ]);
    
  2291.     // Still suspended.
    
  2292.     expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2293. 
    
  2294.     // Flush to skip suspended time.
    
  2295.     Scheduler.unstable_advanceTime(600);
    
  2296.     await advanceTimers(600);
    
  2297. 
    
  2298.     expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2299.   });
    
  2300. 
    
  2301.   describe('startTransition', () => {
    
  2302.     // @gate enableLegacyCache
    
  2303.     it('top level render', async () => {
    
  2304.       function App({page}) {
    
  2305.         return (
    
  2306.           <Suspense fallback={<Text text="Loading..." />}>
    
  2307.             <AsyncText text={page} />
    
  2308.           </Suspense>
    
  2309.         );
    
  2310.       }
    
  2311. 
    
  2312.       // Initial render.
    
  2313.       React.startTransition(() => ReactNoop.render(<App page="A" />));
    
  2314. 
    
  2315.       await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2316.       // Only a short time is needed to unsuspend the initial loading state.
    
  2317.       Scheduler.unstable_advanceTime(400);
    
  2318.       await advanceTimers(400);
    
  2319.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2320. 
    
  2321.       // Later we load the data.
    
  2322.       await act(() => resolveText('A'));
    
  2323.       assertLog(['A']);
    
  2324.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2325. 
    
  2326.       // Start transition.
    
  2327.       React.startTransition(() => ReactNoop.render(<App page="B" />));
    
  2328. 
    
  2329.       await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2330.       Scheduler.unstable_advanceTime(100000);
    
  2331.       await advanceTimers(100000);
    
  2332.       // Even after lots of time has passed, we have still not yet flushed the
    
  2333.       // loading state.
    
  2334.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2335.       // Later we load the data.
    
  2336.       await act(() => resolveText('B'));
    
  2337.       assertLog(['B']);
    
  2338.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2339.     });
    
  2340. 
    
  2341.     // @gate enableLegacyCache
    
  2342.     it('hooks', async () => {
    
  2343.       let transitionToPage;
    
  2344.       function App() {
    
  2345.         const [page, setPage] = React.useState('none');
    
  2346.         transitionToPage = setPage;
    
  2347.         if (page === 'none') {
    
  2348.           return null;
    
  2349.         }
    
  2350.         return (
    
  2351.           <Suspense fallback={<Text text="Loading..." />}>
    
  2352.             <AsyncText text={page} />
    
  2353.           </Suspense>
    
  2354.         );
    
  2355.       }
    
  2356. 
    
  2357.       ReactNoop.render(<App />);
    
  2358.       await waitForAll([]);
    
  2359. 
    
  2360.       // Initial render.
    
  2361.       await act(async () => {
    
  2362.         React.startTransition(() => transitionToPage('A'));
    
  2363. 
    
  2364.         await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2365.         // Only a short time is needed to unsuspend the initial loading state.
    
  2366.         Scheduler.unstable_advanceTime(400);
    
  2367.         await advanceTimers(400);
    
  2368.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2369.       });
    
  2370. 
    
  2371.       // Later we load the data.
    
  2372.       await act(() => resolveText('A'));
    
  2373.       assertLog(['A']);
    
  2374.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2375. 
    
  2376.       // Start transition.
    
  2377.       await act(async () => {
    
  2378.         React.startTransition(() => transitionToPage('B'));
    
  2379. 
    
  2380.         await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2381.         Scheduler.unstable_advanceTime(100000);
    
  2382.         await advanceTimers(100000);
    
  2383.         // Even after lots of time has passed, we have still not yet flushed the
    
  2384.         // loading state.
    
  2385.         expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2386.       });
    
  2387.       // Later we load the data.
    
  2388.       await act(() => resolveText('B'));
    
  2389.       assertLog(['B']);
    
  2390.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2391.     });
    
  2392. 
    
  2393.     // @gate enableLegacyCache
    
  2394.     it('classes', async () => {
    
  2395.       let transitionToPage;
    
  2396.       class App extends React.Component {
    
  2397.         state = {page: 'none'};
    
  2398.         render() {
    
  2399.           transitionToPage = page => this.setState({page});
    
  2400.           const page = this.state.page;
    
  2401.           if (page === 'none') {
    
  2402.             return null;
    
  2403.           }
    
  2404.           return (
    
  2405.             <Suspense fallback={<Text text="Loading..." />}>
    
  2406.               <AsyncText text={page} />
    
  2407.             </Suspense>
    
  2408.           );
    
  2409.         }
    
  2410.       }
    
  2411. 
    
  2412.       ReactNoop.render(<App />);
    
  2413.       await waitForAll([]);
    
  2414. 
    
  2415.       // Initial render.
    
  2416.       await act(async () => {
    
  2417.         React.startTransition(() => transitionToPage('A'));
    
  2418. 
    
  2419.         await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2420.         // Only a short time is needed to unsuspend the initial loading state.
    
  2421.         Scheduler.unstable_advanceTime(400);
    
  2422.         await advanceTimers(400);
    
  2423.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2424.       });
    
  2425. 
    
  2426.       // Later we load the data.
    
  2427.       await act(() => resolveText('A'));
    
  2428.       assertLog(['A']);
    
  2429.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2430. 
    
  2431.       // Start transition.
    
  2432.       await act(async () => {
    
  2433.         React.startTransition(() => transitionToPage('B'));
    
  2434. 
    
  2435.         await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2436.         Scheduler.unstable_advanceTime(100000);
    
  2437.         await advanceTimers(100000);
    
  2438.         // Even after lots of time has passed, we have still not yet flushed the
    
  2439.         // loading state.
    
  2440.         expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2441.       });
    
  2442.       // Later we load the data.
    
  2443.       await act(() => resolveText('B'));
    
  2444.       assertLog(['B']);
    
  2445.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2446.     });
    
  2447.   });
    
  2448. 
    
  2449.   describe('delays transitions when using React.startTransition', () => {
    
  2450.     // @gate enableLegacyCache
    
  2451.     it('top level render', async () => {
    
  2452.       function App({page}) {
    
  2453.         return (
    
  2454.           <Suspense fallback={<Text text="Loading..." />}>
    
  2455.             <AsyncText text={page} />
    
  2456.           </Suspense>
    
  2457.         );
    
  2458.       }
    
  2459. 
    
  2460.       // Initial render.
    
  2461.       React.startTransition(() => ReactNoop.render(<App page="A" />));
    
  2462. 
    
  2463.       await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2464.       // Only a short time is needed to unsuspend the initial loading state.
    
  2465.       Scheduler.unstable_advanceTime(400);
    
  2466.       await advanceTimers(400);
    
  2467.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2468. 
    
  2469.       // Later we load the data.
    
  2470.       await act(() => resolveText('A'));
    
  2471.       assertLog(['A']);
    
  2472.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2473. 
    
  2474.       // Start transition.
    
  2475.       React.startTransition(() => ReactNoop.render(<App page="B" />));
    
  2476. 
    
  2477.       await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2478.       Scheduler.unstable_advanceTime(2999);
    
  2479.       await advanceTimers(2999);
    
  2480.       // Since the timeout is infinite (or effectively infinite),
    
  2481.       // we have still not yet flushed the loading state.
    
  2482.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2483. 
    
  2484.       // Later we load the data.
    
  2485.       await act(() => resolveText('B'));
    
  2486.       assertLog(['B']);
    
  2487.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2488. 
    
  2489.       // Start a long (infinite) transition.
    
  2490.       React.startTransition(() => ReactNoop.render(<App page="C" />));
    
  2491.       await waitForAll(['Suspend! [C]', 'Loading...']);
    
  2492. 
    
  2493.       // Even after lots of time has passed, we have still not yet flushed the
    
  2494.       // loading state.
    
  2495.       Scheduler.unstable_advanceTime(100000);
    
  2496.       await advanceTimers(100000);
    
  2497.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2498.     });
    
  2499. 
    
  2500.     // @gate enableLegacyCache
    
  2501.     it('hooks', async () => {
    
  2502.       let transitionToPage;
    
  2503.       function App() {
    
  2504.         const [page, setPage] = React.useState('none');
    
  2505.         transitionToPage = setPage;
    
  2506.         if (page === 'none') {
    
  2507.           return null;
    
  2508.         }
    
  2509.         return (
    
  2510.           <Suspense fallback={<Text text="Loading..." />}>
    
  2511.             <AsyncText text={page} />
    
  2512.           </Suspense>
    
  2513.         );
    
  2514.       }
    
  2515. 
    
  2516.       ReactNoop.render(<App />);
    
  2517.       await waitForAll([]);
    
  2518. 
    
  2519.       // Initial render.
    
  2520.       await act(async () => {
    
  2521.         React.startTransition(() => transitionToPage('A'));
    
  2522. 
    
  2523.         await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2524.         // Only a short time is needed to unsuspend the initial loading state.
    
  2525.         Scheduler.unstable_advanceTime(400);
    
  2526.         await advanceTimers(400);
    
  2527.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2528.       });
    
  2529. 
    
  2530.       // Later we load the data.
    
  2531.       await act(() => resolveText('A'));
    
  2532.       assertLog(['A']);
    
  2533.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2534. 
    
  2535.       // Start transition.
    
  2536.       await act(async () => {
    
  2537.         React.startTransition(() => transitionToPage('B'));
    
  2538. 
    
  2539.         await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2540. 
    
  2541.         Scheduler.unstable_advanceTime(2999);
    
  2542.         await advanceTimers(2999);
    
  2543.         // Since the timeout is infinite (or effectively infinite),
    
  2544.         // we have still not yet flushed the loading state.
    
  2545.         expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2546.       });
    
  2547. 
    
  2548.       // Later we load the data.
    
  2549.       await act(() => resolveText('B'));
    
  2550.       assertLog(['B']);
    
  2551.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2552. 
    
  2553.       // Start a long (infinite) transition.
    
  2554.       await act(async () => {
    
  2555.         React.startTransition(() => transitionToPage('C'));
    
  2556. 
    
  2557.         await waitForAll(['Suspend! [C]', 'Loading...']);
    
  2558. 
    
  2559.         // Even after lots of time has passed, we have still not yet flushed the
    
  2560.         // loading state.
    
  2561.         Scheduler.unstable_advanceTime(100000);
    
  2562.         await advanceTimers(100000);
    
  2563.         expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2564.       });
    
  2565.     });
    
  2566. 
    
  2567.     // @gate enableLegacyCache
    
  2568.     it('classes', async () => {
    
  2569.       let transitionToPage;
    
  2570.       class App extends React.Component {
    
  2571.         state = {page: 'none'};
    
  2572.         render() {
    
  2573.           transitionToPage = page => this.setState({page});
    
  2574.           const page = this.state.page;
    
  2575.           if (page === 'none') {
    
  2576.             return null;
    
  2577.           }
    
  2578.           return (
    
  2579.             <Suspense fallback={<Text text="Loading..." />}>
    
  2580.               <AsyncText text={page} />
    
  2581.             </Suspense>
    
  2582.           );
    
  2583.         }
    
  2584.       }
    
  2585. 
    
  2586.       ReactNoop.render(<App />);
    
  2587.       await waitForAll([]);
    
  2588. 
    
  2589.       // Initial render.
    
  2590.       await act(async () => {
    
  2591.         React.startTransition(() => transitionToPage('A'));
    
  2592. 
    
  2593.         await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2594.         // Only a short time is needed to unsuspend the initial loading state.
    
  2595.         Scheduler.unstable_advanceTime(400);
    
  2596.         await advanceTimers(400);
    
  2597.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
    
  2598.       });
    
  2599. 
    
  2600.       // Later we load the data.
    
  2601.       await act(() => resolveText('A'));
    
  2602.       assertLog(['A']);
    
  2603.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2604. 
    
  2605.       // Start transition.
    
  2606.       await act(async () => {
    
  2607.         React.startTransition(() => transitionToPage('B'));
    
  2608. 
    
  2609.         await waitForAll(['Suspend! [B]', 'Loading...']);
    
  2610.         Scheduler.unstable_advanceTime(2999);
    
  2611.         await advanceTimers(2999);
    
  2612.         // Since the timeout is infinite (or effectively infinite),
    
  2613.         // we have still not yet flushed the loading state.
    
  2614.         expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
    
  2615.       });
    
  2616. 
    
  2617.       // Later we load the data.
    
  2618.       await act(() => resolveText('B'));
    
  2619.       assertLog(['B']);
    
  2620.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2621. 
    
  2622.       // Start a long (infinite) transition.
    
  2623.       await act(async () => {
    
  2624.         React.startTransition(() => transitionToPage('C'));
    
  2625. 
    
  2626.         await waitForAll(['Suspend! [C]', 'Loading...']);
    
  2627. 
    
  2628.         // Even after lots of time has passed, we have still not yet flushed the
    
  2629.         // loading state.
    
  2630.         Scheduler.unstable_advanceTime(100000);
    
  2631.         await advanceTimers(100000);
    
  2632.         expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  2633.       });
    
  2634.     });
    
  2635.   });
    
  2636. 
    
  2637.   // @gate enableLegacyCache && enableSuspenseAvoidThisFallback
    
  2638.   it('do not show placeholder when updating an avoided boundary with startTransition', async () => {
    
  2639.     function App({page}) {
    
  2640.       return (
    
  2641.         <Suspense fallback={<Text text="Loading..." />}>
    
  2642.           <Text text="Hi!" />
    
  2643.           <Suspense
    
  2644.             fallback={<Text text={'Loading ' + page + '...'} />}
    
  2645.             unstable_avoidThisFallback={true}>
    
  2646.             <AsyncText text={page} />
    
  2647.           </Suspense>
    
  2648.         </Suspense>
    
  2649.       );
    
  2650.     }
    
  2651. 
    
  2652.     // Initial render.
    
  2653.     ReactNoop.render(<App page="A" />);
    
  2654.     await waitForAll(['Hi!', 'Suspend! [A]', 'Loading...']);
    
  2655.     await act(() => resolveText('A'));
    
  2656.     assertLog(['Hi!', 'A']);
    
  2657.     expect(ReactNoop).toMatchRenderedOutput(
    
  2658.       <>
    
  2659.         <span prop="Hi!" />
    
  2660.         <span prop="A" />
    
  2661.       </>,
    
  2662.     );
    
  2663. 
    
  2664.     // Start transition.
    
  2665.     React.startTransition(() => ReactNoop.render(<App page="B" />));
    
  2666. 
    
  2667.     await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
    
  2668. 
    
  2669.     // Suspended
    
  2670.     expect(ReactNoop).toMatchRenderedOutput(
    
  2671.       <>
    
  2672.         <span prop="Hi!" />
    
  2673.         <span prop="A" />
    
  2674.       </>,
    
  2675.     );
    
  2676.     Scheduler.unstable_advanceTime(1800);
    
  2677.     await advanceTimers(1800);
    
  2678.     await waitForAll([]);
    
  2679.     // We should still be suspended here because this loading state should be avoided.
    
  2680.     expect(ReactNoop).toMatchRenderedOutput(
    
  2681.       <>
    
  2682.         <span prop="Hi!" />
    
  2683.         <span prop="A" />
    
  2684.       </>,
    
  2685.     );
    
  2686.     await resolveText('B');
    
  2687.     await waitForAll(['Hi!', 'B']);
    
  2688.     expect(ReactNoop).toMatchRenderedOutput(
    
  2689.       <>
    
  2690.         <span prop="Hi!" />
    
  2691.         <span prop="B" />
    
  2692.       </>,
    
  2693.     );
    
  2694.   });
    
  2695. 
    
  2696.   // @gate enableLegacyCache && enableSuspenseAvoidThisFallback
    
  2697.   it('do not show placeholder when mounting an avoided boundary with startTransition', async () => {
    
  2698.     function App({page}) {
    
  2699.       return (
    
  2700.         <Suspense fallback={<Text text="Loading..." />}>
    
  2701.           <Text text="Hi!" />
    
  2702.           {page === 'A' ? (
    
  2703.             <Text text="A" />
    
  2704.           ) : (
    
  2705.             <Suspense
    
  2706.               fallback={<Text text={'Loading ' + page + '...'} />}
    
  2707.               unstable_avoidThisFallback={true}>
    
  2708.               <AsyncText text={page} />
    
  2709.             </Suspense>
    
  2710.           )}
    
  2711.         </Suspense>
    
  2712.       );
    
  2713.     }
    
  2714. 
    
  2715.     // Initial render.
    
  2716.     ReactNoop.render(<App page="A" />);
    
  2717.     await waitForAll(['Hi!', 'A']);
    
  2718.     expect(ReactNoop).toMatchRenderedOutput(
    
  2719.       <>
    
  2720.         <span prop="Hi!" />
    
  2721.         <span prop="A" />
    
  2722.       </>,
    
  2723.     );
    
  2724. 
    
  2725.     // Start transition.
    
  2726.     React.startTransition(() => ReactNoop.render(<App page="B" />));
    
  2727. 
    
  2728.     await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
    
  2729. 
    
  2730.     // Suspended
    
  2731.     expect(ReactNoop).toMatchRenderedOutput(
    
  2732.       <>
    
  2733.         <span prop="Hi!" />
    
  2734.         <span prop="A" />
    
  2735.       </>,
    
  2736.     );
    
  2737.     Scheduler.unstable_advanceTime(1800);
    
  2738.     await advanceTimers(1800);
    
  2739.     await waitForAll([]);
    
  2740.     // We should still be suspended here because this loading state should be avoided.
    
  2741.     expect(ReactNoop).toMatchRenderedOutput(
    
  2742.       <>
    
  2743.         <span prop="Hi!" />
    
  2744.         <span prop="A" />
    
  2745.       </>,
    
  2746.     );
    
  2747.     await resolveText('B');
    
  2748.     await waitForAll(['Hi!', 'B']);
    
  2749.     expect(ReactNoop).toMatchRenderedOutput(
    
  2750.       <>
    
  2751.         <span prop="Hi!" />
    
  2752.         <span prop="B" />
    
  2753.       </>,
    
  2754.     );
    
  2755.   });
    
  2756. 
    
  2757.   it('regression test: resets current "debug phase" after suspending', async () => {
    
  2758.     function App() {
    
  2759.       return (
    
  2760.         <Suspense fallback="Loading...">
    
  2761.           <Foo suspend={false} />
    
  2762.         </Suspense>
    
  2763.       );
    
  2764.     }
    
  2765. 
    
  2766.     const thenable = {then() {}};
    
  2767. 
    
  2768.     let foo;
    
  2769.     class Foo extends React.Component {
    
  2770.       state = {suspend: false};
    
  2771.       render() {
    
  2772.         foo = this;
    
  2773. 
    
  2774.         if (this.state.suspend) {
    
  2775.           Scheduler.log('Suspend!');
    
  2776.           throw thenable;
    
  2777.         }
    
  2778. 
    
  2779.         return <Text text="Foo" />;
    
  2780.       }
    
  2781.     }
    
  2782. 
    
  2783.     const root = ReactNoop.createRoot();
    
  2784.     await act(() => {
    
  2785.       root.render(<App />);
    
  2786.     });
    
  2787. 
    
  2788.     assertLog(['Foo']);
    
  2789. 
    
  2790.     await act(async () => {
    
  2791.       foo.setState({suspend: true});
    
  2792. 
    
  2793.       // In the regression that this covers, we would neglect to reset the
    
  2794.       // current debug phase after suspending (in the catch block), so React
    
  2795.       // thinks we're still inside the render phase.
    
  2796.       await waitFor(['Suspend!']);
    
  2797. 
    
  2798.       // Then when this setState happens, React would incorrectly fire a warning
    
  2799.       // about updates that happen the render phase (only fired by classes).
    
  2800.       foo.setState({suspend: false});
    
  2801.     });
    
  2802. 
    
  2803.     assertLog([
    
  2804.       // First setState
    
  2805.       'Foo',
    
  2806.     ]);
    
  2807.     expect(root).toMatchRenderedOutput(<span prop="Foo" />);
    
  2808.   });
    
  2809. 
    
  2810.   // @gate enableLegacyCache && enableLegacyHidden
    
  2811.   it('should not render hidden content while suspended on higher pri', async () => {
    
  2812.     function Offscreen() {
    
  2813.       Scheduler.log('Offscreen');
    
  2814.       return 'Offscreen';
    
  2815.     }
    
  2816.     function App({showContent}) {
    
  2817.       React.useLayoutEffect(() => {
    
  2818.         Scheduler.log('Commit');
    
  2819.       });
    
  2820.       return (
    
  2821.         <>
    
  2822.           <LegacyHiddenDiv mode="hidden">
    
  2823.             <Offscreen />
    
  2824.           </LegacyHiddenDiv>
    
  2825.           <Suspense fallback={<Text text="Loading..." />}>
    
  2826.             {showContent ? <AsyncText text="A" ms={2000} /> : null}
    
  2827.           </Suspense>
    
  2828.         </>
    
  2829.       );
    
  2830.     }
    
  2831. 
    
  2832.     // Initial render.
    
  2833.     ReactNoop.render(<App showContent={false} />);
    
  2834.     await waitFor(['Commit']);
    
  2835.     expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
    
  2836. 
    
  2837.     // Start transition.
    
  2838.     React.startTransition(() => {
    
  2839.       ReactNoop.render(<App showContent={true} />);
    
  2840.     });
    
  2841. 
    
  2842.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2843.     await resolveText('A');
    
  2844.     await waitFor(['A', 'Commit']);
    
  2845.     expect(ReactNoop).toMatchRenderedOutput(
    
  2846.       <>
    
  2847.         <div hidden={true} />
    
  2848.         <span prop="A" />
    
  2849.       </>,
    
  2850.     );
    
  2851.     await waitForAll(['Offscreen']);
    
  2852.     expect(ReactNoop).toMatchRenderedOutput(
    
  2853.       <>
    
  2854.         <div hidden={true}>Offscreen</div>
    
  2855.         <span prop="A" />
    
  2856.       </>,
    
  2857.     );
    
  2858.   });
    
  2859. 
    
  2860.   // @gate enableLegacyCache && enableLegacyHidden
    
  2861.   it('should be able to unblock higher pri content before suspended hidden', async () => {
    
  2862.     function Offscreen() {
    
  2863.       Scheduler.log('Offscreen');
    
  2864.       return 'Offscreen';
    
  2865.     }
    
  2866.     function App({showContent}) {
    
  2867.       React.useLayoutEffect(() => {
    
  2868.         Scheduler.log('Commit');
    
  2869.       });
    
  2870.       return (
    
  2871.         <Suspense fallback={<Text text="Loading..." />}>
    
  2872.           <LegacyHiddenDiv mode="hidden">
    
  2873.             <AsyncText text="A" />
    
  2874.             <Offscreen />
    
  2875.           </LegacyHiddenDiv>
    
  2876.           {showContent ? <AsyncText text="A" /> : null}
    
  2877.         </Suspense>
    
  2878.       );
    
  2879.     }
    
  2880. 
    
  2881.     // Initial render.
    
  2882.     ReactNoop.render(<App showContent={false} />);
    
  2883.     await waitFor(['Commit']);
    
  2884.     expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
    
  2885. 
    
  2886.     // Partially render through the hidden content.
    
  2887.     await waitFor(['Suspend! [A]']);
    
  2888. 
    
  2889.     // Start transition.
    
  2890.     React.startTransition(() => {
    
  2891.       ReactNoop.render(<App showContent={true} />);
    
  2892.     });
    
  2893. 
    
  2894.     await waitForAll(['Suspend! [A]', 'Loading...']);
    
  2895.     await resolveText('A');
    
  2896.     await waitFor(['A', 'Commit']);
    
  2897.     expect(ReactNoop).toMatchRenderedOutput(
    
  2898.       <>
    
  2899.         <div hidden={true} />
    
  2900.         <span prop="A" />
    
  2901.       </>,
    
  2902.     );
    
  2903.     await waitForAll(['A', 'Offscreen']);
    
  2904.     expect(ReactNoop).toMatchRenderedOutput(
    
  2905.       <>
    
  2906.         <div hidden={true}>
    
  2907.           <span prop="A" />
    
  2908.           Offscreen
    
  2909.         </div>
    
  2910.         <span prop="A" />
    
  2911.       </>,
    
  2912.     );
    
  2913.   });
    
  2914. 
    
  2915.   // @gate enableLegacyCache
    
  2916.   it(
    
  2917.     'multiple updates originating inside a Suspense boundary at different ' +
    
  2918.       'priority levels are not dropped',
    
  2919.     async () => {
    
  2920.       const {useState} = React;
    
  2921.       const root = ReactNoop.createRoot();
    
  2922. 
    
  2923.       function Parent() {
    
  2924.         return (
    
  2925.           <>
    
  2926.             <Suspense fallback={<Text text="Loading..." />}>
    
  2927.               <Child />
    
  2928.             </Suspense>
    
  2929.           </>
    
  2930.         );
    
  2931.       }
    
  2932. 
    
  2933.       let setText;
    
  2934.       function Child() {
    
  2935.         const [text, _setText] = useState('A');
    
  2936.         setText = _setText;
    
  2937.         return <AsyncText text={text} />;
    
  2938.       }
    
  2939. 
    
  2940.       await seedNextTextCache('A');
    
  2941.       await act(() => {
    
  2942.         root.render(<Parent />);
    
  2943.       });
    
  2944.       assertLog(['A']);
    
  2945.       expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  2946. 
    
  2947.       await act(async () => {
    
  2948.         // Schedule two updates that originate inside the Suspense boundary.
    
  2949.         // The first one causes the boundary to suspend. The second one is at
    
  2950.         // lower priority and unsuspends the tree.
    
  2951.         ReactNoop.discreteUpdates(() => {
    
  2952.           setText('B');
    
  2953.         });
    
  2954.         startTransition(() => {
    
  2955.           setText('C');
    
  2956.         });
    
  2957.         // Assert that neither update has happened yet. Both the high pri and
    
  2958.         // low pri updates are in the queue.
    
  2959.         assertLog([]);
    
  2960. 
    
  2961.         // Resolve this before starting to render so that C doesn't suspend.
    
  2962.         await resolveText('C');
    
  2963.       });
    
  2964.       assertLog([
    
  2965.         // First we attempt the high pri update. It suspends.
    
  2966.         'Suspend! [B]',
    
  2967.         'Loading...',
    
  2968.         // Then we attempt the low pri update, which finishes successfully.
    
  2969.         'C',
    
  2970.       ]);
    
  2971.       expect(root).toMatchRenderedOutput(<span prop="C" />);
    
  2972.     },
    
  2973.   );
    
  2974. 
    
  2975.   // @gate enableLegacyCache
    
  2976.   it(
    
  2977.     'multiple updates originating inside a Suspense boundary at different ' +
    
  2978.       'priority levels are not dropped, including Idle updates',
    
  2979.     async () => {
    
  2980.       const {useState} = React;
    
  2981.       const root = ReactNoop.createRoot();
    
  2982. 
    
  2983.       function Parent() {
    
  2984.         return (
    
  2985.           <>
    
  2986.             <Suspense fallback={<Text text="Loading..." />}>
    
  2987.               <Child />
    
  2988.             </Suspense>
    
  2989.           </>
    
  2990.         );
    
  2991.       }
    
  2992. 
    
  2993.       let setText;
    
  2994.       function Child() {
    
  2995.         const [text, _setText] = useState('A');
    
  2996.         setText = _setText;
    
  2997.         return <AsyncText text={text} />;
    
  2998.       }
    
  2999. 
    
  3000.       await seedNextTextCache('A');
    
  3001.       await act(() => {
    
  3002.         root.render(<Parent />);
    
  3003.       });
    
  3004.       assertLog(['A']);
    
  3005.       expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  3006. 
    
  3007.       await act(async () => {
    
  3008.         // Schedule two updates that originate inside the Suspense boundary.
    
  3009.         // The first one causes the boundary to suspend. The second one is at
    
  3010.         // lower priority and unsuspends it by hiding the async component.
    
  3011.         setText('B');
    
  3012. 
    
  3013.         await resolveText('C');
    
  3014.         ReactNoop.idleUpdates(() => {
    
  3015.           setText('C');
    
  3016.         });
    
  3017. 
    
  3018.         // First we attempt the high pri update. It suspends.
    
  3019.         await waitForPaint(['Suspend! [B]', 'Loading...']);
    
  3020.         expect(root).toMatchRenderedOutput(
    
  3021.           <>
    
  3022.             <span hidden={true} prop="A" />
    
  3023.             <span prop="Loading..." />
    
  3024.           </>,
    
  3025.         );
    
  3026. 
    
  3027.         // Now flush the remaining work. The Idle update successfully finishes.
    
  3028.         await waitForAll(['C']);
    
  3029.         expect(root).toMatchRenderedOutput(<span prop="C" />);
    
  3030.       });
    
  3031.     },
    
  3032.   );
    
  3033. 
    
  3034.   // @gate enableLegacyCache
    
  3035.   it(
    
  3036.     'fallback component can update itself even after a high pri update to ' +
    
  3037.       'the primary tree suspends',
    
  3038.     async () => {
    
  3039.       const {useState} = React;
    
  3040.       const root = ReactNoop.createRoot();
    
  3041. 
    
  3042.       let setAppText;
    
  3043.       function App() {
    
  3044.         const [text, _setText] = useState('A');
    
  3045.         setAppText = _setText;
    
  3046.         return (
    
  3047.           <>
    
  3048.             <Suspense fallback={<Fallback />}>
    
  3049.               <AsyncText text={text} />
    
  3050.             </Suspense>
    
  3051.           </>
    
  3052.         );
    
  3053.       }
    
  3054. 
    
  3055.       let setFallbackText;
    
  3056.       function Fallback() {
    
  3057.         const [text, _setText] = useState('Loading...');
    
  3058.         setFallbackText = _setText;
    
  3059.         return <Text text={text} />;
    
  3060.       }
    
  3061. 
    
  3062.       // Resolve the initial tree
    
  3063.       await seedNextTextCache('A');
    
  3064.       await act(() => {
    
  3065.         root.render(<App />);
    
  3066.       });
    
  3067.       assertLog(['A']);
    
  3068.       expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  3069. 
    
  3070.       await act(async () => {
    
  3071.         // Schedule an update inside the Suspense boundary that suspends.
    
  3072.         setAppText('B');
    
  3073.         await waitForAll(['Suspend! [B]', 'Loading...']);
    
  3074.       });
    
  3075. 
    
  3076.       expect(root).toMatchRenderedOutput(
    
  3077.         <>
    
  3078.           <span hidden={true} prop="A" />
    
  3079.           <span prop="Loading..." />
    
  3080.         </>,
    
  3081.       );
    
  3082. 
    
  3083.       // Schedule a default pri update on the boundary, and a lower pri update
    
  3084.       // on the fallback. We're testing to make sure the fallback can still
    
  3085.       // update even though the primary tree is suspended.
    
  3086.       await act(() => {
    
  3087.         setAppText('C');
    
  3088.         React.startTransition(() => {
    
  3089.           setFallbackText('Still loading...');
    
  3090.         });
    
  3091.       });
    
  3092. 
    
  3093.       assertLog([
    
  3094.         // First try to render the high pri update. Still suspended.
    
  3095.         'Suspend! [C]',
    
  3096.         'Loading...',
    
  3097. 
    
  3098.         // In the expiration times model, once the high pri update suspends,
    
  3099.         // we can't be sure if there's additional work at a lower priority
    
  3100.         // that might unblock the tree. We do know that there's a lower
    
  3101.         // priority update *somewhere* in the entire root, though (the update
    
  3102.         // to the fallback). So we try rendering one more time, just in case.
    
  3103.         // TODO: We shouldn't need to do this with lanes, because we always
    
  3104.         // know exactly which lanes have pending work in each tree.
    
  3105.         'Suspend! [C]',
    
  3106. 
    
  3107.         // Then complete the update to the fallback.
    
  3108.         'Still loading...',
    
  3109.       ]);
    
  3110.       expect(root).toMatchRenderedOutput(
    
  3111.         <>
    
  3112.           <span hidden={true} prop="A" />
    
  3113.           <span prop="Still loading..." />
    
  3114.         </>,
    
  3115.       );
    
  3116.     },
    
  3117.   );
    
  3118. 
    
  3119.   // @gate enableLegacyCache
    
  3120.   it(
    
  3121.     'regression: primary fragment fiber is not always part of setState ' +
    
  3122.       'return path',
    
  3123.     async () => {
    
  3124.       // Reproduces a bug where updates inside a suspended tree are dropped
    
  3125.       // because the fragment fiber we insert to wrap the hidden children is not
    
  3126.       // part of the return path, so it doesn't get marked during setState.
    
  3127.       const {useState} = React;
    
  3128.       const root = ReactNoop.createRoot();
    
  3129. 
    
  3130.       function Parent() {
    
  3131.         return (
    
  3132.           <>
    
  3133.             <Suspense fallback={<Text text="Loading..." />}>
    
  3134.               <Child />
    
  3135.             </Suspense>
    
  3136.           </>
    
  3137.         );
    
  3138.       }
    
  3139. 
    
  3140.       let setText;
    
  3141.       function Child() {
    
  3142.         const [text, _setText] = useState('A');
    
  3143.         setText = _setText;
    
  3144.         return <AsyncText text={text} />;
    
  3145.       }
    
  3146. 
    
  3147.       // Mount an initial tree. Resolve A so that it doesn't suspend.
    
  3148.       await seedNextTextCache('A');
    
  3149.       await act(() => {
    
  3150.         root.render(<Parent />);
    
  3151.       });
    
  3152.       assertLog(['A']);
    
  3153.       // At this point, the setState return path follows current fiber.
    
  3154.       expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  3155. 
    
  3156.       // Schedule another update. This will "flip" the alternate pairs.
    
  3157.       await resolveText('B');
    
  3158.       await act(() => {
    
  3159.         setText('B');
    
  3160.       });
    
  3161.       assertLog(['B']);
    
  3162.       // Now the setState return path follows the *alternate* fiber.
    
  3163.       expect(root).toMatchRenderedOutput(<span prop="B" />);
    
  3164. 
    
  3165.       // Schedule another update. This time, we'll suspend.
    
  3166.       await act(() => {
    
  3167.         setText('C');
    
  3168.       });
    
  3169.       assertLog(['Suspend! [C]', 'Loading...']);
    
  3170. 
    
  3171.       // Commit. This will insert a fragment fiber to wrap around the component
    
  3172.       // that triggered the update.
    
  3173.       await act(async () => {
    
  3174.         await advanceTimers(250);
    
  3175.       });
    
  3176.       // The fragment fiber is part of the current tree, but the setState return
    
  3177.       // path still follows the alternate path. That means the fragment fiber is
    
  3178.       // not part of the return path.
    
  3179.       expect(root).toMatchRenderedOutput(
    
  3180.         <>
    
  3181.           <span hidden={true} prop="B" />
    
  3182.           <span prop="Loading..." />
    
  3183.         </>,
    
  3184.       );
    
  3185. 
    
  3186.       // Update again. This should unsuspend the tree.
    
  3187.       await resolveText('D');
    
  3188.       await act(() => {
    
  3189.         setText('D');
    
  3190.       });
    
  3191.       // Even though the fragment fiber is not part of the return path, we should
    
  3192.       // be able to finish rendering.
    
  3193.       assertLog(['D']);
    
  3194.       expect(root).toMatchRenderedOutput(<span prop="D" />);
    
  3195.     },
    
  3196.   );
    
  3197. 
    
  3198.   // @gate enableLegacyCache
    
  3199.   it(
    
  3200.     'regression: primary fragment fiber is not always part of setState ' +
    
  3201.       'return path (another case)',
    
  3202.     async () => {
    
  3203.       // Reproduces a bug where updates inside a suspended tree are dropped
    
  3204.       // because the fragment fiber we insert to wrap the hidden children is not
    
  3205.       // part of the return path, so it doesn't get marked during setState.
    
  3206.       const {useState} = React;
    
  3207.       const root = ReactNoop.createRoot();
    
  3208. 
    
  3209.       function Parent() {
    
  3210.         return (
    
  3211.           <Suspense fallback={<Text text="Loading..." />}>
    
  3212.             <Child />
    
  3213.           </Suspense>
    
  3214.         );
    
  3215.       }
    
  3216. 
    
  3217.       let setText;
    
  3218.       function Child() {
    
  3219.         const [text, _setText] = useState('A');
    
  3220.         setText = _setText;
    
  3221.         return <AsyncText text={text} />;
    
  3222.       }
    
  3223. 
    
  3224.       // Mount an initial tree. Resolve A so that it doesn't suspend.
    
  3225.       await seedNextTextCache('A');
    
  3226.       await act(() => {
    
  3227.         root.render(<Parent />);
    
  3228.       });
    
  3229.       assertLog(['A']);
    
  3230.       // At this point, the setState return path follows current fiber.
    
  3231.       expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  3232. 
    
  3233.       // Schedule another update. This will "flip" the alternate pairs.
    
  3234.       await resolveText('B');
    
  3235.       await act(() => {
    
  3236.         setText('B');
    
  3237.       });
    
  3238.       assertLog(['B']);
    
  3239.       // Now the setState return path follows the *alternate* fiber.
    
  3240.       expect(root).toMatchRenderedOutput(<span prop="B" />);
    
  3241. 
    
  3242.       // Schedule another update. This time, we'll suspend.
    
  3243.       await act(() => {
    
  3244.         setText('C');
    
  3245.       });
    
  3246.       assertLog(['Suspend! [C]', 'Loading...']);
    
  3247. 
    
  3248.       // Commit. This will insert a fragment fiber to wrap around the component
    
  3249.       // that triggered the update.
    
  3250.       await act(async () => {
    
  3251.         await advanceTimers(250);
    
  3252.       });
    
  3253.       // The fragment fiber is part of the current tree, but the setState return
    
  3254.       // path still follows the alternate path. That means the fragment fiber is
    
  3255.       // not part of the return path.
    
  3256.       expect(root).toMatchRenderedOutput(
    
  3257.         <>
    
  3258.           <span hidden={true} prop="B" />
    
  3259.           <span prop="Loading..." />
    
  3260.         </>,
    
  3261.       );
    
  3262. 
    
  3263.       await act(async () => {
    
  3264.         // Schedule a normal pri update. This will suspend again.
    
  3265.         setText('D');
    
  3266. 
    
  3267.         // And another update at lower priority. This will unblock.
    
  3268.         await resolveText('E');
    
  3269.         ReactNoop.idleUpdates(() => {
    
  3270.           setText('E');
    
  3271.         });
    
  3272.       });
    
  3273.       // Even though the fragment fiber is not part of the return path, we should
    
  3274.       // be able to finish rendering.
    
  3275.       assertLog(['Suspend! [D]', 'E']);
    
  3276.       expect(root).toMatchRenderedOutput(<span prop="E" />);
    
  3277.     },
    
  3278.   );
    
  3279. 
    
  3280.   // @gate enableLegacyCache
    
  3281.   it(
    
  3282.     'after showing fallback, should not flip back to primary content until ' +
    
  3283.       'the update that suspended finishes',
    
  3284.     async () => {
    
  3285.       const {useState, useEffect} = React;
    
  3286.       const root = ReactNoop.createRoot();
    
  3287. 
    
  3288.       let setOuterText;
    
  3289.       function Parent({step}) {
    
  3290.         const [text, _setText] = useState('A');
    
  3291.         setOuterText = _setText;
    
  3292.         return (
    
  3293.           <>
    
  3294.             <Text text={'Outer text: ' + text} />
    
  3295.             <Text text={'Outer step: ' + step} />
    
  3296.             <Suspense fallback={<Text text="Loading..." />}>
    
  3297.               <Child step={step} outerText={text} />
    
  3298.             </Suspense>
    
  3299.           </>
    
  3300.         );
    
  3301.       }
    
  3302. 
    
  3303.       let setInnerText;
    
  3304.       function Child({step, outerText}) {
    
  3305.         const [text, _setText] = useState('A');
    
  3306.         setInnerText = _setText;
    
  3307. 
    
  3308.         // This will log if the component commits in an inconsistent state
    
  3309.         useEffect(() => {
    
  3310.           if (text === outerText) {
    
  3311.             Scheduler.log('Commit Child');
    
  3312.           } else {
    
  3313.             Scheduler.log('FIXME: Texts are inconsistent (tearing)');
    
  3314.           }
    
  3315.         }, [text, outerText]);
    
  3316. 
    
  3317.         return (
    
  3318.           <>
    
  3319.             <AsyncText text={'Inner text: ' + text} />
    
  3320.             <Text text={'Inner step: ' + step} />
    
  3321.           </>
    
  3322.         );
    
  3323.       }
    
  3324. 
    
  3325.       // These always update simultaneously. They must be consistent.
    
  3326.       function setText(text) {
    
  3327.         setOuterText(text);
    
  3328.         setInnerText(text);
    
  3329.       }
    
  3330. 
    
  3331.       // Mount an initial tree. Resolve A so that it doesn't suspend.
    
  3332.       await seedNextTextCache('Inner text: A');
    
  3333.       await act(() => {
    
  3334.         root.render(<Parent step={0} />);
    
  3335.       });
    
  3336.       assertLog([
    
  3337.         'Outer text: A',
    
  3338.         'Outer step: 0',
    
  3339.         'Inner text: A',
    
  3340.         'Inner step: 0',
    
  3341.         'Commit Child',
    
  3342.       ]);
    
  3343.       expect(root).toMatchRenderedOutput(
    
  3344.         <>
    
  3345.           <span prop="Outer text: A" />
    
  3346.           <span prop="Outer step: 0" />
    
  3347.           <span prop="Inner text: A" />
    
  3348.           <span prop="Inner step: 0" />
    
  3349.         </>,
    
  3350.       );
    
  3351. 
    
  3352.       // Update. This causes the inner component to suspend.
    
  3353.       await act(() => {
    
  3354.         setText('B');
    
  3355.       });
    
  3356.       assertLog([
    
  3357.         'Outer text: B',
    
  3358.         'Outer step: 0',
    
  3359.         'Suspend! [Inner text: B]',
    
  3360.         'Loading...',
    
  3361.       ]);
    
  3362.       // Commit the placeholder
    
  3363.       await advanceTimers(250);
    
  3364.       expect(root).toMatchRenderedOutput(
    
  3365.         <>
    
  3366.           <span prop="Outer text: B" />
    
  3367.           <span prop="Outer step: 0" />
    
  3368.           <span hidden={true} prop="Inner text: A" />
    
  3369.           <span hidden={true} prop="Inner step: 0" />
    
  3370.           <span prop="Loading..." />
    
  3371.         </>,
    
  3372.       );
    
  3373. 
    
  3374.       // Schedule a high pri update on the parent.
    
  3375.       await act(() => {
    
  3376.         ReactNoop.discreteUpdates(() => {
    
  3377.           root.render(<Parent step={1} />);
    
  3378.         });
    
  3379.       });
    
  3380. 
    
  3381.       // Only the outer part can update. The inner part should still show a
    
  3382.       // fallback because we haven't finished loading B yet. Otherwise, the
    
  3383.       // inner text would be inconsistent with the outer text.
    
  3384.       assertLog([
    
  3385.         'Outer text: B',
    
  3386.         'Outer step: 1',
    
  3387.         'Suspend! [Inner text: B]',
    
  3388.         'Loading...',
    
  3389.       ]);
    
  3390.       expect(root).toMatchRenderedOutput(
    
  3391.         <>
    
  3392.           <span prop="Outer text: B" />
    
  3393.           <span prop="Outer step: 1" />
    
  3394.           <span hidden={true} prop="Inner text: A" />
    
  3395.           <span hidden={true} prop="Inner step: 0" />
    
  3396.           <span prop="Loading..." />
    
  3397.         </>,
    
  3398.       );
    
  3399. 
    
  3400.       // Now finish resolving the inner text
    
  3401.       await act(async () => {
    
  3402.         await resolveText('Inner text: B');
    
  3403.       });
    
  3404.       assertLog(['Inner text: B', 'Inner step: 1', 'Commit Child']);
    
  3405.       expect(root).toMatchRenderedOutput(
    
  3406.         <>
    
  3407.           <span prop="Outer text: B" />
    
  3408.           <span prop="Outer step: 1" />
    
  3409.           <span prop="Inner text: B" />
    
  3410.           <span prop="Inner step: 1" />
    
  3411.         </>,
    
  3412.       );
    
  3413.     },
    
  3414.   );
    
  3415. 
    
  3416.   // @gate enableLegacyCache
    
  3417.   it('a high pri update can unhide a boundary that suspended at a different level', async () => {
    
  3418.     const {useState, useEffect} = React;
    
  3419.     const root = ReactNoop.createRoot();
    
  3420. 
    
  3421.     let setOuterText;
    
  3422.     function Parent({step}) {
    
  3423.       const [text, _setText] = useState('A');
    
  3424.       setOuterText = _setText;
    
  3425.       return (
    
  3426.         <>
    
  3427.           <Text text={'Outer: ' + text + step} />
    
  3428.           <Suspense fallback={<Text text="Loading..." />}>
    
  3429.             <Child step={step} outerText={text} />
    
  3430.           </Suspense>
    
  3431.         </>
    
  3432.       );
    
  3433.     }
    
  3434. 
    
  3435.     let setInnerText;
    
  3436.     function Child({step, outerText}) {
    
  3437.       const [text, _setText] = useState('A');
    
  3438.       setInnerText = _setText;
    
  3439. 
    
  3440.       // This will log if the component commits in an inconsistent state
    
  3441.       useEffect(() => {
    
  3442.         if (text === outerText) {
    
  3443.           Scheduler.log('Commit Child');
    
  3444.         } else {
    
  3445.           Scheduler.log('FIXME: Texts are inconsistent (tearing)');
    
  3446.         }
    
  3447.       }, [text, outerText]);
    
  3448. 
    
  3449.       return (
    
  3450.         <>
    
  3451.           <AsyncText text={'Inner: ' + text + step} />
    
  3452.         </>
    
  3453.       );
    
  3454.     }
    
  3455. 
    
  3456.     // These always update simultaneously. They must be consistent.
    
  3457.     function setText(text) {
    
  3458.       setOuterText(text);
    
  3459.       setInnerText(text);
    
  3460.     }
    
  3461. 
    
  3462.     // Mount an initial tree. Resolve A so that it doesn't suspend.
    
  3463.     await seedNextTextCache('Inner: A0');
    
  3464.     await act(() => {
    
  3465.       root.render(<Parent step={0} />);
    
  3466.     });
    
  3467.     assertLog(['Outer: A0', 'Inner: A0', 'Commit Child']);
    
  3468.     expect(root).toMatchRenderedOutput(
    
  3469.       <>
    
  3470.         <span prop="Outer: A0" />
    
  3471.         <span prop="Inner: A0" />
    
  3472.       </>,
    
  3473.     );
    
  3474. 
    
  3475.     // Update. This causes the inner component to suspend.
    
  3476.     await act(() => {
    
  3477.       setText('B');
    
  3478.     });
    
  3479.     assertLog(['Outer: B0', 'Suspend! [Inner: B0]', 'Loading...']);
    
  3480.     // Commit the placeholder
    
  3481.     await advanceTimers(250);
    
  3482.     expect(root).toMatchRenderedOutput(
    
  3483.       <>
    
  3484.         <span prop="Outer: B0" />
    
  3485.         <span hidden={true} prop="Inner: A0" />
    
  3486.         <span prop="Loading..." />
    
  3487.       </>,
    
  3488.     );
    
  3489. 
    
  3490.     // Schedule a high pri update on the parent. This will unblock the content.
    
  3491.     await resolveText('Inner: B1');
    
  3492.     await act(() => {
    
  3493.       ReactNoop.discreteUpdates(() => {
    
  3494.         root.render(<Parent step={1} />);
    
  3495.       });
    
  3496.     });
    
  3497. 
    
  3498.     assertLog(['Outer: B1', 'Inner: B1', 'Commit Child']);
    
  3499.     expect(root).toMatchRenderedOutput(
    
  3500.       <>
    
  3501.         <span prop="Outer: B1" />
    
  3502.         <span prop="Inner: B1" />
    
  3503.       </>,
    
  3504.     );
    
  3505.   });
    
  3506. 
    
  3507.   // @gate enableLegacyCache
    
  3508.   // @gate forceConcurrentByDefaultForTesting
    
  3509.   it('regression: ping at high priority causes update to be dropped', async () => {
    
  3510.     const {useState, useTransition} = React;
    
  3511. 
    
  3512.     let setTextA;
    
  3513.     function A() {
    
  3514.       const [textA, _setTextA] = useState('A');
    
  3515.       setTextA = _setTextA;
    
  3516.       return (
    
  3517.         <Suspense fallback={<Text text="Loading..." />}>
    
  3518.           <AsyncText text={textA} />
    
  3519.         </Suspense>
    
  3520.       );
    
  3521.     }
    
  3522. 
    
  3523.     let setTextB;
    
  3524.     let startTransitionFromB;
    
  3525.     function B() {
    
  3526.       const [textB, _setTextB] = useState('B');
    
  3527.       // eslint-disable-next-line no-unused-vars
    
  3528.       const [_, _startTransition] = useTransition();
    
  3529.       startTransitionFromB = _startTransition;
    
  3530.       setTextB = _setTextB;
    
  3531.       return (
    
  3532.         <Suspense fallback={<Text text="Loading..." />}>
    
  3533.           <AsyncText text={textB} />
    
  3534.         </Suspense>
    
  3535.       );
    
  3536.     }
    
  3537. 
    
  3538.     function App() {
    
  3539.       return (
    
  3540.         <>
    
  3541.           <A />
    
  3542.           <B />
    
  3543.         </>
    
  3544.       );
    
  3545.     }
    
  3546. 
    
  3547.     const root = ReactNoop.createRoot();
    
  3548.     await act(async () => {
    
  3549.       await seedNextTextCache('A');
    
  3550.       await seedNextTextCache('B');
    
  3551.       root.render(<App />);
    
  3552.     });
    
  3553.     assertLog(['A', 'B']);
    
  3554.     expect(root).toMatchRenderedOutput(
    
  3555.       <>
    
  3556.         <span prop="A" />
    
  3557.         <span prop="B" />
    
  3558.       </>,
    
  3559.     );
    
  3560. 
    
  3561.     await act(async () => {
    
  3562.       // Triggers suspense at normal pri
    
  3563.       setTextA('A1');
    
  3564.       // Triggers in an unrelated tree at a different pri
    
  3565.       startTransitionFromB(() => {
    
  3566.         // Update A again so that it doesn't suspend on A1. That way we can ping
    
  3567.         // the A1 update without also pinging this one. This is a workaround
    
  3568.         // because there's currently no way to render at a lower priority (B2)
    
  3569.         // without including all updates at higher priority (A1).
    
  3570.         setTextA('A2');
    
  3571.         setTextB('B2');
    
  3572.       });
    
  3573. 
    
  3574.       await waitFor([
    
  3575.         'B',
    
  3576.         'Suspend! [A1]',
    
  3577.         'Loading...',
    
  3578. 
    
  3579.         'Suspend! [A2]',
    
  3580.         'Loading...',
    
  3581.         'Suspend! [B2]',
    
  3582.         'Loading...',
    
  3583.       ]);
    
  3584.       expect(root).toMatchRenderedOutput(
    
  3585.         <>
    
  3586.           <span hidden={true} prop="A" />
    
  3587.           <span prop="Loading..." />
    
  3588.           <span prop="B" />
    
  3589.         </>,
    
  3590.       );
    
  3591. 
    
  3592.       await resolveText('A1');
    
  3593.       await waitFor(['A1']);
    
  3594.     });
    
  3595.     assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
    
  3596.     expect(root).toMatchRenderedOutput(
    
  3597.       <>
    
  3598.         <span prop="A1" />
    
  3599.         <span prop="B" />
    
  3600.       </>,
    
  3601.     );
    
  3602. 
    
  3603.     await act(async () => {
    
  3604.       await resolveText('A2');
    
  3605.       await resolveText('B2');
    
  3606.     });
    
  3607.     assertLog(['A2', 'B2']);
    
  3608.     expect(root).toMatchRenderedOutput(
    
  3609.       <>
    
  3610.         <span prop="A2" />
    
  3611.         <span prop="B2" />
    
  3612.       </>,
    
  3613.     );
    
  3614.   });
    
  3615. 
    
  3616.   // Regression: https://github.com/facebook/react/issues/18486
    
  3617.   // @gate enableLegacyCache
    
  3618.   it('does not get stuck in pending state with render phase updates', async () => {
    
  3619.     let setTextWithShortTransition;
    
  3620.     let setTextWithLongTransition;
    
  3621. 
    
  3622.     function App() {
    
  3623.       const [isPending1, startShortTransition] = React.useTransition();
    
  3624.       const [isPending2, startLongTransition] = React.useTransition();
    
  3625.       const isPending = isPending1 || isPending2;
    
  3626.       const [text, setText] = React.useState('');
    
  3627.       const [mirror, setMirror] = React.useState('');
    
  3628. 
    
  3629.       if (text !== mirror) {
    
  3630.         // Render phase update was needed to repro the bug.
    
  3631.         setMirror(text);
    
  3632.       }
    
  3633. 
    
  3634.       setTextWithShortTransition = value => {
    
  3635.         startShortTransition(() => {
    
  3636.           setText(value);
    
  3637.         });
    
  3638.       };
    
  3639.       setTextWithLongTransition = value => {
    
  3640.         startLongTransition(() => {
    
  3641.           setText(value);
    
  3642.         });
    
  3643.       };
    
  3644. 
    
  3645.       return (
    
  3646.         <>
    
  3647.           {isPending ? <Text text="Pending..." /> : null}
    
  3648.           {text !== '' ? <AsyncText text={text} /> : <Text text={text} />}
    
  3649.         </>
    
  3650.       );
    
  3651.     }
    
  3652. 
    
  3653.     function Root() {
    
  3654.       return (
    
  3655.         <Suspense fallback={<Text text="Loading..." />}>
    
  3656.           <App />
    
  3657.         </Suspense>
    
  3658.       );
    
  3659.     }
    
  3660. 
    
  3661.     const root = ReactNoop.createRoot();
    
  3662.     await act(() => {
    
  3663.       root.render(<Root />);
    
  3664.     });
    
  3665.     assertLog(['']);
    
  3666.     expect(root).toMatchRenderedOutput(<span prop="" />);
    
  3667. 
    
  3668.     // Update to "a". That will suspend.
    
  3669.     await act(async () => {
    
  3670.       setTextWithShortTransition('a');
    
  3671.       await waitForAll(['Pending...', '', 'Suspend! [a]', 'Loading...']);
    
  3672.     });
    
  3673.     assertLog([]);
    
  3674.     expect(root).toMatchRenderedOutput(
    
  3675.       <>
    
  3676.         <span prop="Pending..." />
    
  3677.         <span prop="" />
    
  3678.       </>,
    
  3679.     );
    
  3680. 
    
  3681.     // Update to "b". That will suspend, too.
    
  3682.     await act(async () => {
    
  3683.       setTextWithLongTransition('b');
    
  3684.       await waitForAll([
    
  3685.         // Neither is resolved yet.
    
  3686.         'Pending...',
    
  3687.         '',
    
  3688.         'Suspend! [b]',
    
  3689.         'Loading...',
    
  3690.       ]);
    
  3691.     });
    
  3692.     assertLog([]);
    
  3693.     expect(root).toMatchRenderedOutput(
    
  3694.       <>
    
  3695.         <span prop="Pending..." />
    
  3696.         <span prop="" />
    
  3697.       </>,
    
  3698.     );
    
  3699. 
    
  3700.     // Resolve "a". But "b" is still pending.
    
  3701.     await act(async () => {
    
  3702.       await resolveText('a');
    
  3703. 
    
  3704.       await waitForAll(['Suspend! [b]', 'Loading...']);
    
  3705.       expect(root).toMatchRenderedOutput(
    
  3706.         <>
    
  3707.           <span prop="Pending..." />
    
  3708.           <span prop="" />
    
  3709.         </>,
    
  3710.       );
    
  3711. 
    
  3712.       // Resolve "b". This should remove the pending state.
    
  3713.       await act(async () => {
    
  3714.         await resolveText('b');
    
  3715.       });
    
  3716.       assertLog(['b']);
    
  3717.       // The bug was that the pending state got stuck forever.
    
  3718.       expect(root).toMatchRenderedOutput(<span prop="b" />);
    
  3719.     });
    
  3720.   });
    
  3721. 
    
  3722.   // @gate enableLegacyCache
    
  3723.   it('regression: #18657', async () => {
    
  3724.     const {useState} = React;
    
  3725. 
    
  3726.     let setText;
    
  3727.     function App() {
    
  3728.       const [text, _setText] = useState('A');
    
  3729.       setText = _setText;
    
  3730.       return <AsyncText text={text} />;
    
  3731.     }
    
  3732. 
    
  3733.     const root = ReactNoop.createRoot();
    
  3734.     await act(async () => {
    
  3735.       await seedNextTextCache('A');
    
  3736.       root.render(
    
  3737.         <Suspense fallback={<Text text="Loading..." />}>
    
  3738.           <App />
    
  3739.         </Suspense>,
    
  3740.       );
    
  3741.     });
    
  3742.     assertLog(['A']);
    
  3743.     expect(root).toMatchRenderedOutput(<span prop="A" />);
    
  3744. 
    
  3745.     await act(async () => {
    
  3746.       setText('B');
    
  3747.       ReactNoop.idleUpdates(() => {
    
  3748.         setText('C');
    
  3749.       });
    
  3750. 
    
  3751.       // Suspend the first update. This triggers an immediate fallback because
    
  3752.       // it wasn't wrapped in startTransition.
    
  3753.       await waitForPaint(['Suspend! [B]', 'Loading...']);
    
  3754.       expect(root).toMatchRenderedOutput(
    
  3755.         <>
    
  3756.           <span hidden={true} prop="A" />
    
  3757.           <span prop="Loading..." />
    
  3758.         </>,
    
  3759.       );
    
  3760. 
    
  3761.       // Once the fallback renders, proceed to the Idle update. This will
    
  3762.       // also suspend.
    
  3763.       await waitForAll(['Suspend! [C]']);
    
  3764.     });
    
  3765. 
    
  3766.     // Finish loading B.
    
  3767.     await act(async () => {
    
  3768.       setText('B');
    
  3769.       await resolveText('B');
    
  3770.     });
    
  3771.     // We did not try to render the Idle update again because there have been no
    
  3772.     // additional updates since the last time it was attempted.
    
  3773.     assertLog(['B']);
    
  3774.     expect(root).toMatchRenderedOutput(<span prop="B" />);
    
  3775. 
    
  3776.     // Finish loading C.
    
  3777.     await act(async () => {
    
  3778.       setText('C');
    
  3779.       await resolveText('C');
    
  3780.     });
    
  3781.     assertLog(['C']);
    
  3782.     expect(root).toMatchRenderedOutput(<span prop="C" />);
    
  3783.   });
    
  3784. 
    
  3785.   // @gate enableLegacyCache
    
  3786.   it('retries have lower priority than normal updates', async () => {
    
  3787.     const {useState} = React;
    
  3788. 
    
  3789.     let setText;
    
  3790.     function UpdatingText() {
    
  3791.       const [text, _setText] = useState('A');
    
  3792.       setText = _setText;
    
  3793.       return <Text text={text} />;
    
  3794.     }
    
  3795. 
    
  3796.     const root = ReactNoop.createRoot();
    
  3797.     await act(() => {
    
  3798.       root.render(
    
  3799.         <>
    
  3800.           <UpdatingText />
    
  3801.           <Suspense fallback={<Text text="Loading..." />}>
    
  3802.             <AsyncText text="Async" />
    
  3803.           </Suspense>
    
  3804.         </>,
    
  3805.       );
    
  3806.     });
    
  3807.     assertLog(['A', 'Suspend! [Async]', 'Loading...']);
    
  3808.     expect(root).toMatchRenderedOutput(
    
  3809.       <>
    
  3810.         <span prop="A" />
    
  3811.         <span prop="Loading..." />
    
  3812.       </>,
    
  3813.     );
    
  3814. 
    
  3815.     await act(async () => {
    
  3816.       // Resolve the promise. This will trigger a retry.
    
  3817.       await resolveText('Async');
    
  3818.       // Before the retry happens, schedule a new update.
    
  3819.       setText('B');
    
  3820. 
    
  3821.       // The update should be allowed to finish before the retry is attempted.
    
  3822.       await waitForPaint(['B']);
    
  3823.       expect(root).toMatchRenderedOutput(
    
  3824.         <>
    
  3825.           <span prop="B" />
    
  3826.           <span prop="Loading..." />
    
  3827.         </>,
    
  3828.       );
    
  3829.     });
    
  3830.     // Then do the retry.
    
  3831.     assertLog(['Async']);
    
  3832.     expect(root).toMatchRenderedOutput(
    
  3833.       <>
    
  3834.         <span prop="B" />
    
  3835.         <span prop="Async" />
    
  3836.       </>,
    
  3837.     );
    
  3838.   });
    
  3839. 
    
  3840.   // @gate enableLegacyCache
    
  3841.   it('should fire effect clean-up when deleting suspended tree', async () => {
    
  3842.     const {useEffect} = React;
    
  3843. 
    
  3844.     function App({show}) {
    
  3845.       return (
    
  3846.         <Suspense fallback={<Text text="Loading..." />}>
    
  3847.           <Child />
    
  3848.           {show && <AsyncText text="Async" />}
    
  3849.         </Suspense>
    
  3850.       );
    
  3851.     }
    
  3852. 
    
  3853.     function Child() {
    
  3854.       useEffect(() => {
    
  3855.         Scheduler.log('Mount Child');
    
  3856.         return () => {
    
  3857.           Scheduler.log('Unmount Child');
    
  3858.         };
    
  3859.       }, []);
    
  3860.       return <span prop="Child" />;
    
  3861.     }
    
  3862. 
    
  3863.     const root = ReactNoop.createRoot();
    
  3864. 
    
  3865.     await act(() => {
    
  3866.       root.render(<App show={false} />);
    
  3867.     });
    
  3868.     assertLog(['Mount Child']);
    
  3869.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  3870. 
    
  3871.     await act(() => {
    
  3872.       root.render(<App show={true} />);
    
  3873.     });
    
  3874.     assertLog(['Suspend! [Async]', 'Loading...']);
    
  3875.     expect(root).toMatchRenderedOutput(
    
  3876.       <>
    
  3877.         <span hidden={true} prop="Child" />
    
  3878.         <span prop="Loading..." />
    
  3879.       </>,
    
  3880.     );
    
  3881. 
    
  3882.     await act(() => {
    
  3883.       root.render(null);
    
  3884.     });
    
  3885.     assertLog(['Unmount Child']);
    
  3886.   });
    
  3887. 
    
  3888.   // @gate enableLegacyCache
    
  3889.   it('should fire effect clean-up when deleting suspended tree (legacy)', async () => {
    
  3890.     const {useEffect} = React;
    
  3891. 
    
  3892.     function App({show}) {
    
  3893.       return (
    
  3894.         <Suspense fallback={<Text text="Loading..." />}>
    
  3895.           <Child />
    
  3896.           {show && <AsyncText text="Async" />}
    
  3897.         </Suspense>
    
  3898.       );
    
  3899.     }
    
  3900. 
    
  3901.     function Child() {
    
  3902.       useEffect(() => {
    
  3903.         Scheduler.log('Mount Child');
    
  3904.         return () => {
    
  3905.           Scheduler.log('Unmount Child');
    
  3906.         };
    
  3907.       }, []);
    
  3908.       return <span prop="Child" />;
    
  3909.     }
    
  3910. 
    
  3911.     const root = ReactNoop.createLegacyRoot();
    
  3912. 
    
  3913.     await act(() => {
    
  3914.       root.render(<App show={false} />);
    
  3915.     });
    
  3916.     assertLog(['Mount Child']);
    
  3917.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  3918. 
    
  3919.     await act(() => {
    
  3920.       root.render(<App show={true} />);
    
  3921.     });
    
  3922.     assertLog(['Suspend! [Async]', 'Loading...']);
    
  3923.     expect(root).toMatchRenderedOutput(
    
  3924.       <>
    
  3925.         <span hidden={true} prop="Child" />
    
  3926.         <span prop="Loading..." />
    
  3927.       </>,
    
  3928.     );
    
  3929. 
    
  3930.     await act(() => {
    
  3931.       root.render(null);
    
  3932.     });
    
  3933.     assertLog(['Unmount Child']);
    
  3934.   });
    
  3935. 
    
  3936.   // @gate enableLegacyCache
    
  3937.   it(
    
  3938.     'regression test: pinging synchronously within the render phase ' +
    
  3939.       'does not unwind the stack',
    
  3940.     async () => {
    
  3941.       // This is a regression test that reproduces a very specific scenario that
    
  3942.       // used to cause a crash.
    
  3943.       const thenable = {
    
  3944.         then(resolve) {
    
  3945.           resolve('hi');
    
  3946.         },
    
  3947.         status: 'pending',
    
  3948.       };
    
  3949. 
    
  3950.       function ImmediatelyPings() {
    
  3951.         if (thenable.status === 'pending') {
    
  3952.           thenable.status = 'fulfilled';
    
  3953.           throw thenable;
    
  3954.         }
    
  3955.         return <Text text="Hi" />;
    
  3956.       }
    
  3957. 
    
  3958.       function App({showMore}) {
    
  3959.         return (
    
  3960.           <div>
    
  3961.             <Suspense fallback={<Text text="Loading..." />}>
    
  3962.               {showMore ? (
    
  3963.                 <>
    
  3964.                   <AsyncText text="Async" />
    
  3965.                 </>
    
  3966.               ) : null}
    
  3967.             </Suspense>
    
  3968.             {showMore ? (
    
  3969.               <Suspense>
    
  3970.                 <ImmediatelyPings />
    
  3971.               </Suspense>
    
  3972.             ) : null}
    
  3973.           </div>
    
  3974.         );
    
  3975.       }
    
  3976. 
    
  3977.       // Initial render. This mounts a Suspense boundary, so that in the next
    
  3978.       // update we can trigger a "suspend with delay" scenario.
    
  3979.       const root = ReactNoop.createRoot();
    
  3980.       await act(() => {
    
  3981.         root.render(<App showMore={false} />);
    
  3982.       });
    
  3983.       assertLog([]);
    
  3984.       expect(root).toMatchRenderedOutput(<div />);
    
  3985. 
    
  3986.       // Update. This will cause two separate trees to suspend. The first tree
    
  3987.       // will be inside an already mounted Suspense boundary, so it will trigger
    
  3988.       // a "suspend with delay". The second tree will be a new Suspense
    
  3989.       // boundary, but the thenable that is thrown will immediately call its
    
  3990.       // ping listener.
    
  3991.       //
    
  3992.       // Before the bug was fixed, this would lead to a `prepareFreshStack` call
    
  3993.       // that unwinds the work-in-progress stack. When that code was written, it
    
  3994.       // was expected that pings always happen from an asynchronous task (or
    
  3995.       // microtask). But this test shows an example where that's not the case.
    
  3996.       //
    
  3997.       // The fix was to check if we're in the render phase before calling
    
  3998.       // `prepareFreshStack`.
    
  3999.       await act(() => {
    
  4000.         root.render(<App showMore={true} />);
    
  4001.       });
    
  4002.       assertLog(['Suspend! [Async]', 'Loading...', 'Hi']);
    
  4003.       expect(root).toMatchRenderedOutput(
    
  4004.         <div>
    
  4005.           <span prop="Loading..." />
    
  4006.           <span prop="Hi" />
    
  4007.         </div>,
    
  4008.       );
    
  4009.     },
    
  4010.   );
    
  4011. });