1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. 'use strict';
    
  12. 
    
  13. let React;
    
  14. let ReactNoop;
    
  15. let Scheduler;
    
  16. let Suspense;
    
  17. let useState;
    
  18. let useLayoutEffect;
    
  19. let useTransition;
    
  20. let startTransition;
    
  21. let act;
    
  22. let getCacheForType;
    
  23. let waitForAll;
    
  24. let waitFor;
    
  25. let waitForPaint;
    
  26. let assertLog;
    
  27. 
    
  28. let caches;
    
  29. let seededCache;
    
  30. 
    
  31. describe('ReactTransition', () => {
    
  32.   beforeEach(() => {
    
  33.     jest.resetModules();
    
  34.     React = require('react');
    
  35.     ReactNoop = require('react-noop-renderer');
    
  36.     Scheduler = require('scheduler');
    
  37.     useState = React.useState;
    
  38.     useLayoutEffect = React.useLayoutEffect;
    
  39.     useTransition = React.useTransition;
    
  40.     Suspense = React.Suspense;
    
  41.     startTransition = React.startTransition;
    
  42.     getCacheForType = React.unstable_getCacheForType;
    
  43.     act = require('internal-test-utils').act;
    
  44. 
    
  45.     const InternalTestUtils = require('internal-test-utils');
    
  46.     waitForAll = InternalTestUtils.waitForAll;
    
  47.     waitFor = InternalTestUtils.waitFor;
    
  48.     waitForPaint = InternalTestUtils.waitForPaint;
    
  49.     assertLog = InternalTestUtils.assertLog;
    
  50. 
    
  51.     caches = [];
    
  52.     seededCache = null;
    
  53.   });
    
  54. 
    
  55.   function createTextCache() {
    
  56.     if (seededCache !== null) {
    
  57.       // Trick to seed a cache before it exists.
    
  58.       // TODO: Need a built-in API to seed data before the initial render (i.e.
    
  59.       // not a refresh because nothing has mounted yet).
    
  60.       const cache = seededCache;
    
  61.       seededCache = null;
    
  62.       return cache;
    
  63.     }
    
  64. 
    
  65.     const data = new Map();
    
  66.     const version = caches.length + 1;
    
  67.     const cache = {
    
  68.       version,
    
  69.       data,
    
  70.       resolve(text) {
    
  71.         const record = data.get(text);
    
  72.         if (record === undefined) {
    
  73.           const newRecord = {
    
  74.             status: 'resolved',
    
  75.             value: text,
    
  76.           };
    
  77.           data.set(text, newRecord);
    
  78.         } else if (record.status === 'pending') {
    
  79.           const thenable = record.value;
    
  80.           record.status = 'resolved';
    
  81.           record.value = text;
    
  82.           thenable.pings.forEach(t => t());
    
  83.         }
    
  84.       },
    
  85.       reject(text, error) {
    
  86.         const record = data.get(text);
    
  87.         if (record === undefined) {
    
  88.           const newRecord = {
    
  89.             status: 'rejected',
    
  90.             value: error,
    
  91.           };
    
  92.           data.set(text, newRecord);
    
  93.         } else if (record.status === 'pending') {
    
  94.           const thenable = record.value;
    
  95.           record.status = 'rejected';
    
  96.           record.value = error;
    
  97.           thenable.pings.forEach(t => t());
    
  98.         }
    
  99.       },
    
  100.     };
    
  101.     caches.push(cache);
    
  102.     return cache;
    
  103.   }
    
  104. 
    
  105.   function readText(text) {
    
  106.     const textCache = getCacheForType(createTextCache);
    
  107.     const record = textCache.data.get(text);
    
  108.     if (record !== undefined) {
    
  109.       switch (record.status) {
    
  110.         case 'pending':
    
  111.           Scheduler.log(`Suspend! [${text}]`);
    
  112.           throw record.value;
    
  113.         case 'rejected':
    
  114.           Scheduler.log(`Error! [${text}]`);
    
  115.           throw record.value;
    
  116.         case 'resolved':
    
  117.           return textCache.version;
    
  118.       }
    
  119.     } else {
    
  120.       Scheduler.log(`Suspend! [${text}]`);
    
  121. 
    
  122.       const thenable = {
    
  123.         pings: [],
    
  124.         then(resolve) {
    
  125.           if (newRecord.status === 'pending') {
    
  126.             thenable.pings.push(resolve);
    
  127.           } else {
    
  128.             Promise.resolve().then(() => resolve(newRecord.value));
    
  129.           }
    
  130.         },
    
  131.       };
    
  132. 
    
  133.       const newRecord = {
    
  134.         status: 'pending',
    
  135.         value: thenable,
    
  136.       };
    
  137.       textCache.data.set(text, newRecord);
    
  138. 
    
  139.       throw thenable;
    
  140.     }
    
  141.   }
    
  142. 
    
  143.   function Text({text}) {
    
  144.     Scheduler.log(text);
    
  145.     return text;
    
  146.   }
    
  147. 
    
  148.   function AsyncText({text}) {
    
  149.     readText(text);
    
  150.     Scheduler.log(text);
    
  151.     return text;
    
  152.   }
    
  153. 
    
  154.   function seedNextTextCache(text) {
    
  155.     if (seededCache === null) {
    
  156.       seededCache = createTextCache();
    
  157.     }
    
  158.     seededCache.resolve(text);
    
  159.   }
    
  160. 
    
  161.   function resolveText(text) {
    
  162.     if (caches.length === 0) {
    
  163.       throw Error('Cache does not exist.');
    
  164.     } else {
    
  165.       // Resolve the most recently created cache. An older cache can by
    
  166.       // resolved with `caches[index].resolve(text)`.
    
  167.       caches[caches.length - 1].resolve(text);
    
  168.     }
    
  169.   }
    
  170. 
    
  171.   // @gate enableLegacyCache
    
  172.   test('isPending works even if called from outside an input event', async () => {
    
  173.     let start;
    
  174.     function App() {
    
  175.       const [show, setShow] = useState(false);
    
  176.       const [isPending, _start] = useTransition();
    
  177.       start = () => _start(() => setShow(true));
    
  178.       return (
    
  179.         <Suspense fallback={<Text text="Loading..." />}>
    
  180.           {isPending ? <Text text="Pending..." /> : null}
    
  181.           {show ? <AsyncText text="Async" /> : <Text text="(empty)" />}
    
  182.         </Suspense>
    
  183.       );
    
  184.     }
    
  185. 
    
  186.     const root = ReactNoop.createRoot();
    
  187. 
    
  188.     await act(() => {
    
  189.       root.render(<App />);
    
  190.     });
    
  191.     assertLog(['(empty)']);
    
  192.     expect(root).toMatchRenderedOutput('(empty)');
    
  193. 
    
  194.     await act(async () => {
    
  195.       start();
    
  196. 
    
  197.       await waitForAll([
    
  198.         'Pending...',
    
  199.         '(empty)',
    
  200.         'Suspend! [Async]',
    
  201.         'Loading...',
    
  202.       ]);
    
  203. 
    
  204.       expect(root).toMatchRenderedOutput('Pending...(empty)');
    
  205. 
    
  206.       await resolveText('Async');
    
  207.     });
    
  208.     assertLog(['Async']);
    
  209.     expect(root).toMatchRenderedOutput('Async');
    
  210.   });
    
  211. 
    
  212.   // @gate enableLegacyCache
    
  213.   test(
    
  214.     'when multiple transitions update the same queue, only the most recent ' +
    
  215.       'one is allowed to finish (no intermediate states)',
    
  216.     async () => {
    
  217.       let update;
    
  218.       function App() {
    
  219.         const [isContentPending, startContentChange] = useTransition();
    
  220.         const [label, setLabel] = useState('A');
    
  221.         const [contents, setContents] = useState('A');
    
  222.         update = value => {
    
  223.           ReactNoop.discreteUpdates(() => {
    
  224.             setLabel(value);
    
  225.             startContentChange(() => {
    
  226.               setContents(value);
    
  227.             });
    
  228.           });
    
  229.         };
    
  230.         return (
    
  231.           <>
    
  232.             <Text
    
  233.               text={
    
  234.                 label + ' label' + (isContentPending ? ' (loading...)' : '')
    
  235.               }
    
  236.             />
    
  237.             <div>
    
  238.               <Suspense fallback={<Text text="Loading..." />}>
    
  239.                 <AsyncText text={contents + ' content'} />
    
  240.               </Suspense>
    
  241.             </div>
    
  242.           </>
    
  243.         );
    
  244.       }
    
  245. 
    
  246.       // Initial render
    
  247.       const root = ReactNoop.createRoot();
    
  248.       await act(() => {
    
  249.         seedNextTextCache('A content');
    
  250.         root.render(<App />);
    
  251.       });
    
  252.       assertLog(['A label', 'A content']);
    
  253.       expect(root).toMatchRenderedOutput(
    
  254.         <>
    
  255.           A label<div>A content</div>
    
  256.         </>,
    
  257.       );
    
  258. 
    
  259.       // Switch to B
    
  260.       await act(() => {
    
  261.         update('B');
    
  262.       });
    
  263.       assertLog([
    
  264.         // Commit pending state
    
  265.         'B label (loading...)',
    
  266.         'A content',
    
  267. 
    
  268.         // Attempt to render B, but it suspends
    
  269.         'B label',
    
  270.         'Suspend! [B content]',
    
  271.         'Loading...',
    
  272.       ]);
    
  273.       // This is a refresh transition so it shouldn't show a fallback
    
  274.       expect(root).toMatchRenderedOutput(
    
  275.         <>
    
  276.           B label (loading...)<div>A content</div>
    
  277.         </>,
    
  278.       );
    
  279. 
    
  280.       // Before B finishes loading, switch to C
    
  281.       await act(() => {
    
  282.         update('C');
    
  283.       });
    
  284.       assertLog([
    
  285.         // Commit pending state
    
  286.         'C label (loading...)',
    
  287.         'A content',
    
  288. 
    
  289.         // Attempt to render C, but it suspends
    
  290.         'C label',
    
  291.         'Suspend! [C content]',
    
  292.         'Loading...',
    
  293.       ]);
    
  294.       expect(root).toMatchRenderedOutput(
    
  295.         <>
    
  296.           C label (loading...)<div>A content</div>
    
  297.         </>,
    
  298.       );
    
  299. 
    
  300.       // Finish loading B. But we're not allowed to render B because it's
    
  301.       // entangled with C. So we're still pending.
    
  302.       await act(() => {
    
  303.         resolveText('B content');
    
  304.       });
    
  305.       assertLog([
    
  306.         // Attempt to render C, but it suspends
    
  307.         'C label',
    
  308.         'Suspend! [C content]',
    
  309.         'Loading...',
    
  310.       ]);
    
  311.       expect(root).toMatchRenderedOutput(
    
  312.         <>
    
  313.           C label (loading...)<div>A content</div>
    
  314.         </>,
    
  315.       );
    
  316. 
    
  317.       // Now finish loading C. This is the terminal update, so it can finish.
    
  318.       await act(() => {
    
  319.         resolveText('C content');
    
  320.       });
    
  321.       assertLog(['C label', 'C content']);
    
  322.       expect(root).toMatchRenderedOutput(
    
  323.         <>
    
  324.           C label<div>C content</div>
    
  325.         </>,
    
  326.       );
    
  327.     },
    
  328.   );
    
  329. 
    
  330.   // Same as previous test, but for class update queue.
    
  331.   // @gate enableLegacyCache
    
  332.   test(
    
  333.     'when multiple transitions update the same queue, only the most recent ' +
    
  334.       'one is allowed to finish (no intermediate states) (classes)',
    
  335.     async () => {
    
  336.       let update;
    
  337.       class App extends React.Component {
    
  338.         state = {
    
  339.           label: 'A',
    
  340.           contents: 'A',
    
  341.         };
    
  342.         render() {
    
  343.           update = value => {
    
  344.             ReactNoop.discreteUpdates(() => {
    
  345.               this.setState({label: value});
    
  346.               startTransition(() => {
    
  347.                 this.setState({contents: value});
    
  348.               });
    
  349.             });
    
  350.           };
    
  351.           const label = this.state.label;
    
  352.           const contents = this.state.contents;
    
  353.           const isContentPending = label !== contents;
    
  354.           return (
    
  355.             <>
    
  356.               <Text
    
  357.                 text={
    
  358.                   label + ' label' + (isContentPending ? ' (loading...)' : '')
    
  359.                 }
    
  360.               />
    
  361.               <div>
    
  362.                 <Suspense fallback={<Text text="Loading..." />}>
    
  363.                   <AsyncText text={contents + ' content'} />
    
  364.                 </Suspense>
    
  365.               </div>
    
  366.             </>
    
  367.           );
    
  368.         }
    
  369.       }
    
  370. 
    
  371.       // Initial render
    
  372.       const root = ReactNoop.createRoot();
    
  373.       await act(() => {
    
  374.         seedNextTextCache('A content');
    
  375.         root.render(<App />);
    
  376.       });
    
  377.       assertLog(['A label', 'A content']);
    
  378.       expect(root).toMatchRenderedOutput(
    
  379.         <>
    
  380.           A label<div>A content</div>
    
  381.         </>,
    
  382.       );
    
  383. 
    
  384.       // Switch to B
    
  385.       await act(() => {
    
  386.         update('B');
    
  387.       });
    
  388.       assertLog([
    
  389.         // Commit pending state
    
  390.         'B label (loading...)',
    
  391.         'A content',
    
  392. 
    
  393.         // Attempt to render B, but it suspends
    
  394.         'B label',
    
  395.         'Suspend! [B content]',
    
  396.         'Loading...',
    
  397.       ]);
    
  398.       // This is a refresh transition so it shouldn't show a fallback
    
  399.       expect(root).toMatchRenderedOutput(
    
  400.         <>
    
  401.           B label (loading...)<div>A content</div>
    
  402.         </>,
    
  403.       );
    
  404. 
    
  405.       // Before B finishes loading, switch to C
    
  406.       await act(() => {
    
  407.         update('C');
    
  408.       });
    
  409.       assertLog([
    
  410.         // Commit pending state
    
  411.         'C label (loading...)',
    
  412.         'A content',
    
  413. 
    
  414.         // Attempt to render C, but it suspends
    
  415.         'C label',
    
  416.         'Suspend! [C content]',
    
  417.         'Loading...',
    
  418.       ]);
    
  419.       expect(root).toMatchRenderedOutput(
    
  420.         <>
    
  421.           C label (loading...)<div>A content</div>
    
  422.         </>,
    
  423.       );
    
  424. 
    
  425.       // Finish loading B. But we're not allowed to render B because it's
    
  426.       // entangled with C. So we're still pending.
    
  427.       await act(() => {
    
  428.         resolveText('B content');
    
  429.       });
    
  430.       assertLog([
    
  431.         // Attempt to render C, but it suspends
    
  432.         'C label',
    
  433.         'Suspend! [C content]',
    
  434.         'Loading...',
    
  435.       ]);
    
  436.       expect(root).toMatchRenderedOutput(
    
  437.         <>
    
  438.           C label (loading...)<div>A content</div>
    
  439.         </>,
    
  440.       );
    
  441. 
    
  442.       // Now finish loading C. This is the terminal update, so it can finish.
    
  443.       await act(() => {
    
  444.         resolveText('C content');
    
  445.       });
    
  446.       assertLog(['C label', 'C content']);
    
  447.       expect(root).toMatchRenderedOutput(
    
  448.         <>
    
  449.           C label<div>C content</div>
    
  450.         </>,
    
  451.       );
    
  452.     },
    
  453.   );
    
  454. 
    
  455.   // @gate enableLegacyCache
    
  456.   test(
    
  457.     'when multiple transitions update overlapping queues, all the transitions ' +
    
  458.       'across all the queues are entangled',
    
  459.     async () => {
    
  460.       let setShowA;
    
  461.       let setShowB;
    
  462.       let setShowC;
    
  463.       function App() {
    
  464.         const [showA, _setShowA] = useState(false);
    
  465.         const [showB, _setShowB] = useState(false);
    
  466.         const [showC, _setShowC] = useState(false);
    
  467.         setShowA = _setShowA;
    
  468.         setShowB = _setShowB;
    
  469.         setShowC = _setShowC;
    
  470. 
    
  471.         // Only one of these children should be visible at a time. Except
    
  472.         // instead of being modeled as a single state, it's three separate
    
  473.         // states that are updated simultaneously. This may seem a bit
    
  474.         // contrived, but it's more common than you might think. Usually via
    
  475.         // a framework or indirection. For example, consider a tooltip manager
    
  476.         // that only shows a single tooltip at a time. Or a router that
    
  477.         // highlights links to the active route.
    
  478.         return (
    
  479.           <>
    
  480.             <Suspense fallback={<Text text="Loading..." />}>
    
  481.               {showA ? <AsyncText text="A" /> : null}
    
  482.               {showB ? <AsyncText text="B" /> : null}
    
  483.               {showC ? <AsyncText text="C" /> : null}
    
  484.             </Suspense>
    
  485.           </>
    
  486.         );
    
  487.       }
    
  488. 
    
  489.       // Initial render. Start with all children hidden.
    
  490.       const root = ReactNoop.createRoot();
    
  491.       await act(() => {
    
  492.         root.render(<App />);
    
  493.       });
    
  494.       assertLog([]);
    
  495.       expect(root).toMatchRenderedOutput(null);
    
  496. 
    
  497.       // Switch to A.
    
  498.       await act(() => {
    
  499.         startTransition(() => {
    
  500.           setShowA(true);
    
  501.         });
    
  502.       });
    
  503.       assertLog(['Suspend! [A]', 'Loading...']);
    
  504.       expect(root).toMatchRenderedOutput(null);
    
  505. 
    
  506.       // Before A loads, switch to B. This should entangle A with B.
    
  507.       await act(() => {
    
  508.         startTransition(() => {
    
  509.           setShowA(false);
    
  510.           setShowB(true);
    
  511.         });
    
  512.       });
    
  513.       assertLog(['Suspend! [B]', 'Loading...']);
    
  514.       expect(root).toMatchRenderedOutput(null);
    
  515. 
    
  516.       // Before A or B loads, switch to C. This should entangle C with B, and
    
  517.       // transitively entangle C with A.
    
  518.       await act(() => {
    
  519.         startTransition(() => {
    
  520.           setShowB(false);
    
  521.           setShowC(true);
    
  522.         });
    
  523.       });
    
  524.       assertLog(['Suspend! [C]', 'Loading...']);
    
  525.       expect(root).toMatchRenderedOutput(null);
    
  526. 
    
  527.       // Now the data starts resolving out of order.
    
  528. 
    
  529.       // First resolve B. This will attempt to render C, since everything is
    
  530.       // entangled.
    
  531.       await act(() => {
    
  532.         startTransition(() => {
    
  533.           resolveText('B');
    
  534.         });
    
  535.       });
    
  536.       assertLog(['Suspend! [C]', 'Loading...']);
    
  537.       expect(root).toMatchRenderedOutput(null);
    
  538. 
    
  539.       // Now resolve A. Again, this will attempt to render C, since everything
    
  540.       // is entangled.
    
  541.       await act(() => {
    
  542.         startTransition(() => {
    
  543.           resolveText('A');
    
  544.         });
    
  545.       });
    
  546.       assertLog(['Suspend! [C]', 'Loading...']);
    
  547.       expect(root).toMatchRenderedOutput(null);
    
  548. 
    
  549.       // Finally, resolve C. This time we can finish.
    
  550.       await act(() => {
    
  551.         startTransition(() => {
    
  552.           resolveText('C');
    
  553.         });
    
  554.       });
    
  555.       assertLog(['C']);
    
  556.       expect(root).toMatchRenderedOutput('C');
    
  557.     },
    
  558.   );
    
  559. 
    
  560.   // @gate enableLegacyCache
    
  561.   test('interrupt a refresh transition if a new transition is scheduled', async () => {
    
  562.     const root = ReactNoop.createRoot();
    
  563. 
    
  564.     await act(() => {
    
  565.       root.render(
    
  566.         <>
    
  567.           <Suspense fallback={<Text text="Loading..." />} />
    
  568.           <Text text="Initial" />
    
  569.         </>,
    
  570.       );
    
  571.     });
    
  572.     assertLog(['Initial']);
    
  573.     expect(root).toMatchRenderedOutput('Initial');
    
  574. 
    
  575.     await act(async () => {
    
  576.       // Start a refresh transition
    
  577.       startTransition(() => {
    
  578.         root.render(
    
  579.           <>
    
  580.             <Suspense fallback={<Text text="Loading..." />}>
    
  581.               <AsyncText text="Async" />
    
  582.             </Suspense>
    
  583.             <Text text="After Suspense" />
    
  584.             <Text text="Sibling" />
    
  585.           </>,
    
  586.         );
    
  587.       });
    
  588. 
    
  589.       // Partially render it.
    
  590.       await waitFor([
    
  591.         // Once we the update suspends, we know it's a refresh transition,
    
  592.         // because the Suspense boundary has already mounted.
    
  593.         'Suspend! [Async]',
    
  594.         'Loading...',
    
  595.         'After Suspense',
    
  596.       ]);
    
  597. 
    
  598.       // Schedule a new transition
    
  599.       startTransition(async () => {
    
  600.         root.render(
    
  601.           <>
    
  602.             <Suspense fallback={<Text text="Loading..." />} />
    
  603.             <Text text="Updated" />
    
  604.           </>,
    
  605.         );
    
  606.       });
    
  607.     });
    
  608. 
    
  609.     // Because the first one is going to suspend regardless, we should
    
  610.     // immediately switch to rendering the new transition.
    
  611.     assertLog(['Updated']);
    
  612.     expect(root).toMatchRenderedOutput('Updated');
    
  613.   });
    
  614. 
    
  615.   // @gate enableLegacyCache
    
  616.   test(
    
  617.     "interrupt a refresh transition when something suspends and we've " +
    
  618.       'already bailed out on another transition in a parent',
    
  619.     async () => {
    
  620.       let setShouldSuspend;
    
  621. 
    
  622.       function Parent({children}) {
    
  623.         const [shouldHideInParent, _setShouldHideInParent] = useState(false);
    
  624.         setShouldHideInParent = _setShouldHideInParent;
    
  625.         Scheduler.log('shouldHideInParent: ' + shouldHideInParent);
    
  626.         if (shouldHideInParent) {
    
  627.           return <Text text="(empty)" />;
    
  628.         }
    
  629.         return children;
    
  630.       }
    
  631. 
    
  632.       let setShouldHideInParent;
    
  633.       function App() {
    
  634.         const [shouldSuspend, _setShouldSuspend] = useState(false);
    
  635.         setShouldSuspend = _setShouldSuspend;
    
  636.         return (
    
  637.           <>
    
  638.             <Text text="A" />
    
  639.             <Parent>
    
  640.               <Suspense fallback={<Text text="Loading..." />}>
    
  641.                 {shouldSuspend ? <AsyncText text="Async" /> : null}
    
  642.               </Suspense>
    
  643.             </Parent>
    
  644.             <Text text="B" />
    
  645.             <Text text="C" />
    
  646.           </>
    
  647.         );
    
  648.       }
    
  649. 
    
  650.       const root = ReactNoop.createRoot();
    
  651. 
    
  652.       await act(async () => {
    
  653.         root.render(<App />);
    
  654.         await waitForAll(['A', 'shouldHideInParent: false', 'B', 'C']);
    
  655.         expect(root).toMatchRenderedOutput('ABC');
    
  656. 
    
  657.         // Schedule an update
    
  658.         startTransition(() => {
    
  659.           setShouldSuspend(true);
    
  660.         });
    
  661. 
    
  662.         // Now we need to trigger schedule another transition in a different
    
  663.         // lane from the first one. At the time this was written, all transitions are worked on
    
  664.         // simultaneously, unless a transition was already in progress when a
    
  665.         // new one was scheduled. So, partially render the first transition.
    
  666.         await waitFor(['A']);
    
  667. 
    
  668.         // Now schedule a second transition. We won't interrupt the first one.
    
  669.         React.startTransition(() => {
    
  670.           setShouldHideInParent(true);
    
  671.         });
    
  672.         // Continue rendering the first transition.
    
  673.         await waitFor([
    
  674.           'shouldHideInParent: false',
    
  675.           'Suspend! [Async]',
    
  676.           'Loading...',
    
  677.           'B',
    
  678.         ]);
    
  679.         // Should not have committed loading state
    
  680.         expect(root).toMatchRenderedOutput('ABC');
    
  681. 
    
  682.         // At this point, we've processed the parent update queue, so we know
    
  683.         // that it has a pending update from the second transition, even though
    
  684.         // we skipped it during this render. And we know this is a refresh
    
  685.         // transition, because we had to render a loading state. So the next
    
  686.         // time we re-enter the work loop (we don't interrupt immediately, we
    
  687.         // just wait for the next time slice), we should throw out the
    
  688.         // suspended first transition and try the second one.
    
  689.         await waitForPaint(['shouldHideInParent: true', '(empty)']);
    
  690.         expect(root).toMatchRenderedOutput('A(empty)BC');
    
  691. 
    
  692.         // Since the two transitions are not entangled, we then later go back
    
  693.         // and finish retry the first transition. Not really relevant to this
    
  694.         // test but I'll assert the result anyway.
    
  695.         await waitForAll([
    
  696.           'A',
    
  697.           'shouldHideInParent: true',
    
  698.           '(empty)',
    
  699.           'B',
    
  700.           'C',
    
  701.         ]);
    
  702.         expect(root).toMatchRenderedOutput('A(empty)BC');
    
  703.       });
    
  704.     },
    
  705.   );
    
  706. 
    
  707.   // @gate enableLegacyCache
    
  708.   test(
    
  709.     'interrupt a refresh transition when something suspends and a parent ' +
    
  710.       'component received an interleaved update after its queue was processed',
    
  711.     async () => {
    
  712.       // Title is confusing so I'll try to explain further: This is similar to
    
  713.       // the previous test, except instead of skipped over a transition update
    
  714.       // in a parent, the parent receives an interleaved update *after* its
    
  715.       // begin phase has already finished.
    
  716. 
    
  717.       function App({shouldSuspend, step}) {
    
  718.         return (
    
  719.           <>
    
  720.             <Text text={`A${step}`} />
    
  721.             <Suspense fallback={<Text text="Loading..." />}>
    
  722.               {shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
    
  723.             </Suspense>
    
  724.             <Text text={`B${step}`} />
    
  725.             <Text text={`C${step}`} />
    
  726.           </>
    
  727.         );
    
  728.       }
    
  729. 
    
  730.       const root = ReactNoop.createRoot();
    
  731. 
    
  732.       await act(() => {
    
  733.         root.render(<App shouldSuspend={false} step={0} />);
    
  734.       });
    
  735.       assertLog(['A0', 'B0', 'C0']);
    
  736.       expect(root).toMatchRenderedOutput('A0B0C0');
    
  737. 
    
  738.       await act(async () => {
    
  739.         // This update will suspend.
    
  740.         startTransition(() => {
    
  741.           root.render(<App shouldSuspend={true} step={1} />);
    
  742.         });
    
  743.         // Flush past the root, but stop before the async component.
    
  744.         await waitFor(['A1']);
    
  745. 
    
  746.         // Schedule another transition on the root, which already completed.
    
  747.         startTransition(() => {
    
  748.           root.render(<App shouldSuspend={false} step={2} />);
    
  749.         });
    
  750.         // We'll keep working on the first update.
    
  751.         await waitFor([
    
  752.           // Now the async component suspends
    
  753.           'Suspend! [Async]',
    
  754.           'Loading...',
    
  755.           'B1',
    
  756.         ]);
    
  757.         // Should not have committed loading state
    
  758.         expect(root).toMatchRenderedOutput('A0B0C0');
    
  759. 
    
  760.         // After suspending, should abort the first update and switch to the
    
  761.         // second update. So, C1 should not appear in the log.
    
  762.         // TODO: This should work even if React does not yield to the main
    
  763.         // thread. Should use same mechanism as selective hydration to interrupt
    
  764.         // the render before the end of the current slice of work.
    
  765.         await waitForAll(['A2', 'B2', 'C2']);
    
  766. 
    
  767.         expect(root).toMatchRenderedOutput('A2B2C2');
    
  768.       });
    
  769.     },
    
  770.   );
    
  771. 
    
  772.   it('should render normal pri updates scheduled after transitions before transitions', async () => {
    
  773.     let updateTransitionPri;
    
  774.     let updateNormalPri;
    
  775.     function App() {
    
  776.       const [normalPri, setNormalPri] = useState(0);
    
  777.       const [transitionPri, setTransitionPri] = useState(0);
    
  778.       updateTransitionPri = () =>
    
  779.         startTransition(() => setTransitionPri(n => n + 1));
    
  780.       updateNormalPri = () => setNormalPri(n => n + 1);
    
  781. 
    
  782.       useLayoutEffect(() => {
    
  783.         Scheduler.log('Commit');
    
  784.       });
    
  785. 
    
  786.       return (
    
  787.         <Suspense fallback={<Text text="Loading..." />}>
    
  788.           <Text text={'Transition pri: ' + transitionPri} />
    
  789.           {', '}
    
  790.           <Text text={'Normal pri: ' + normalPri} />
    
  791.         </Suspense>
    
  792.       );
    
  793.     }
    
  794. 
    
  795.     const root = ReactNoop.createRoot();
    
  796.     await act(() => {
    
  797.       root.render(<App />);
    
  798.     });
    
  799. 
    
  800.     // Initial render.
    
  801.     assertLog(['Transition pri: 0', 'Normal pri: 0', 'Commit']);
    
  802.     expect(root).toMatchRenderedOutput('Transition pri: 0, Normal pri: 0');
    
  803. 
    
  804.     await act(() => {
    
  805.       updateTransitionPri();
    
  806.       updateNormalPri();
    
  807.     });
    
  808. 
    
  809.     assertLog([
    
  810.       // Normal update first.
    
  811.       'Transition pri: 0',
    
  812.       'Normal pri: 1',
    
  813.       'Commit',
    
  814. 
    
  815.       // Then transition update.
    
  816.       'Transition pri: 1',
    
  817.       'Normal pri: 1',
    
  818.       'Commit',
    
  819.     ]);
    
  820.     expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
    
  821.   });
    
  822. 
    
  823.   // @gate enableLegacyCache
    
  824.   it('should render normal pri updates before transition suspense retries', async () => {
    
  825.     let updateTransitionPri;
    
  826.     let updateNormalPri;
    
  827.     function App() {
    
  828.       const [transitionPri, setTransitionPri] = useState(false);
    
  829.       const [normalPri, setNormalPri] = useState(0);
    
  830. 
    
  831.       updateTransitionPri = () => startTransition(() => setTransitionPri(true));
    
  832.       updateNormalPri = () => setNormalPri(n => n + 1);
    
  833. 
    
  834.       useLayoutEffect(() => {
    
  835.         Scheduler.log('Commit');
    
  836.       });
    
  837. 
    
  838.       return (
    
  839.         <Suspense fallback={<Text text="Loading..." />}>
    
  840.           {transitionPri ? <AsyncText text="Async" /> : <Text text="(empty)" />}
    
  841.           {', '}
    
  842.           <Text text={'Normal pri: ' + normalPri} />
    
  843.         </Suspense>
    
  844.       );
    
  845.     }
    
  846. 
    
  847.     const root = ReactNoop.createRoot();
    
  848.     await act(() => {
    
  849.       root.render(<App />);
    
  850.     });
    
  851. 
    
  852.     // Initial render.
    
  853.     assertLog(['(empty)', 'Normal pri: 0', 'Commit']);
    
  854.     expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
    
  855. 
    
  856.     await act(() => {
    
  857.       updateTransitionPri();
    
  858.     });
    
  859. 
    
  860.     assertLog([
    
  861.       // Suspend.
    
  862.       'Suspend! [Async]',
    
  863.       'Loading...',
    
  864.     ]);
    
  865.     expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
    
  866. 
    
  867.     await act(async () => {
    
  868.       await resolveText('Async');
    
  869.       updateNormalPri();
    
  870.     });
    
  871. 
    
  872.     assertLog([
    
  873.       // Normal pri update.
    
  874.       '(empty)',
    
  875.       'Normal pri: 1',
    
  876.       'Commit',
    
  877. 
    
  878.       // Promise resolved, retry flushed.
    
  879.       'Async',
    
  880.       'Normal pri: 1',
    
  881.       'Commit',
    
  882.     ]);
    
  883.     expect(root).toMatchRenderedOutput('Async, Normal pri: 1');
    
  884.   });
    
  885. 
    
  886.   it('should not interrupt transitions with normal pri updates', async () => {
    
  887.     let updateNormalPri;
    
  888.     let updateTransitionPri;
    
  889.     function App() {
    
  890.       const [transitionPri, setTransitionPri] = useState(0);
    
  891.       const [normalPri, setNormalPri] = useState(0);
    
  892.       updateTransitionPri = () =>
    
  893.         startTransition(() => setTransitionPri(n => n + 1));
    
  894.       updateNormalPri = () => setNormalPri(n => n + 1);
    
  895. 
    
  896.       useLayoutEffect(() => {
    
  897.         Scheduler.log('Commit');
    
  898.       });
    
  899.       return (
    
  900.         <>
    
  901.           <Text text={'Transition pri: ' + transitionPri} />
    
  902.           {', '}
    
  903.           <Text text={'Normal pri: ' + normalPri} />
    
  904.         </>
    
  905.       );
    
  906.     }
    
  907. 
    
  908.     const root = ReactNoop.createRoot();
    
  909.     await act(() => {
    
  910.       root.render(<App />);
    
  911.     });
    
  912.     assertLog(['Transition pri: 0', 'Normal pri: 0', 'Commit']);
    
  913.     expect(root).toMatchRenderedOutput('Transition pri: 0, Normal pri: 0');
    
  914. 
    
  915.     await act(async () => {
    
  916.       updateTransitionPri();
    
  917. 
    
  918.       await waitFor([
    
  919.         // Start transition update.
    
  920.         'Transition pri: 1',
    
  921.       ]);
    
  922. 
    
  923.       // Schedule normal pri update during transition update.
    
  924.       // This should not interrupt.
    
  925.       updateNormalPri();
    
  926.     });
    
  927. 
    
  928.     if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  929.       assertLog([
    
  930.         'Normal pri: 0',
    
  931.         'Commit',
    
  932. 
    
  933.         // Normal pri update.
    
  934.         'Transition pri: 1',
    
  935.         'Normal pri: 1',
    
  936.         'Commit',
    
  937.       ]);
    
  938.     } else {
    
  939.       assertLog([
    
  940.         // Finish transition update.
    
  941.         'Normal pri: 0',
    
  942.         'Commit',
    
  943. 
    
  944.         // Normal pri update.
    
  945.         'Transition pri: 1',
    
  946.         'Normal pri: 1',
    
  947.         'Commit',
    
  948.       ]);
    
  949.     }
    
  950. 
    
  951.     expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
    
  952.   });
    
  953. 
    
  954.   it('tracks two pending flags for nested startTransition (#26226)', async () => {
    
  955.     let update;
    
  956.     function App() {
    
  957.       const [isPendingA, startTransitionA] = useTransition();
    
  958.       const [isPendingB, startTransitionB] = useTransition();
    
  959.       const [state, setState] = useState(0);
    
  960. 
    
  961.       update = function () {
    
  962.         startTransitionA(() => {
    
  963.           startTransitionB(() => {
    
  964.             setState(1);
    
  965.           });
    
  966.         });
    
  967.       };
    
  968. 
    
  969.       return (
    
  970.         <>
    
  971.           <Text text={state} />
    
  972.           {', '}
    
  973.           <Text text={'A ' + isPendingA} />
    
  974.           {', '}
    
  975.           <Text text={'B ' + isPendingB} />
    
  976.         </>
    
  977.       );
    
  978.     }
    
  979.     const root = ReactNoop.createRoot();
    
  980.     await act(async () => {
    
  981.       root.render(<App />);
    
  982.     });
    
  983.     assertLog([0, 'A false', 'B false']);
    
  984.     expect(root).toMatchRenderedOutput('0, A false, B false');
    
  985. 
    
  986.     await act(async () => {
    
  987.       update();
    
  988.     });
    
  989.     assertLog([0, 'A true', 'B true', 1, 'A false', 'B false']);
    
  990.     expect(root).toMatchRenderedOutput('1, A false, B false');
    
  991.   });
    
  992. });