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.  * @jest-environment node
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let React;
    
  13. let ReactNoop;
    
  14. let Scheduler;
    
  15. let act;
    
  16. let readText;
    
  17. let resolveText;
    
  18. let startTransition;
    
  19. let useState;
    
  20. let useEffect;
    
  21. let assertLog;
    
  22. let waitFor;
    
  23. let waitForAll;
    
  24. let unstable_waitForExpired;
    
  25. 
    
  26. describe('ReactExpiration', () => {
    
  27.   beforeEach(() => {
    
  28.     jest.resetModules();
    
  29. 
    
  30.     React = require('react');
    
  31.     ReactNoop = require('react-noop-renderer');
    
  32.     Scheduler = require('scheduler');
    
  33.     act = require('internal-test-utils').act;
    
  34.     startTransition = React.startTransition;
    
  35.     useState = React.useState;
    
  36.     useEffect = React.useEffect;
    
  37. 
    
  38.     const InternalTestUtils = require('internal-test-utils');
    
  39.     assertLog = InternalTestUtils.assertLog;
    
  40.     waitFor = InternalTestUtils.waitFor;
    
  41.     waitForAll = InternalTestUtils.waitForAll;
    
  42.     unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
    
  43. 
    
  44.     const textCache = new Map();
    
  45. 
    
  46.     readText = text => {
    
  47.       const record = textCache.get(text);
    
  48.       if (record !== undefined) {
    
  49.         switch (record.status) {
    
  50.           case 'pending':
    
  51.             throw record.promise;
    
  52.           case 'rejected':
    
  53.             throw Error('Failed to load: ' + text);
    
  54.           case 'resolved':
    
  55.             return text;
    
  56.         }
    
  57.       } else {
    
  58.         let ping;
    
  59.         const promise = new Promise(resolve => (ping = resolve));
    
  60.         const newRecord = {
    
  61.           status: 'pending',
    
  62.           ping: ping,
    
  63.           promise,
    
  64.         };
    
  65.         textCache.set(text, newRecord);
    
  66.         throw promise;
    
  67.       }
    
  68.     };
    
  69. 
    
  70.     resolveText = text => {
    
  71.       const record = textCache.get(text);
    
  72.       if (record !== undefined) {
    
  73.         if (record.status === 'pending') {
    
  74.           Scheduler.log(`Promise resolved [${text}]`);
    
  75.           record.ping();
    
  76.           record.ping = null;
    
  77.           record.status = 'resolved';
    
  78.           clearTimeout(record.promise._timer);
    
  79.           record.promise = null;
    
  80.         }
    
  81.       } else {
    
  82.         const newRecord = {
    
  83.           ping: null,
    
  84.           status: 'resolved',
    
  85.           promise: null,
    
  86.         };
    
  87.         textCache.set(text, newRecord);
    
  88.       }
    
  89.     };
    
  90.   });
    
  91. 
    
  92.   function Text(props) {
    
  93.     Scheduler.log(props.text);
    
  94.     return props.text;
    
  95.   }
    
  96. 
    
  97.   function AsyncText(props) {
    
  98.     const text = props.text;
    
  99.     try {
    
  100.       readText(text);
    
  101.       Scheduler.log(text);
    
  102.       return text;
    
  103.     } catch (promise) {
    
  104.       if (typeof promise.then === 'function') {
    
  105.         Scheduler.log(`Suspend! [${text}]`);
    
  106.         if (typeof props.ms === 'number' && promise._timer === undefined) {
    
  107.           promise._timer = setTimeout(() => {
    
  108.             resolveText(text);
    
  109.           }, props.ms);
    
  110.         }
    
  111.       } else {
    
  112.         Scheduler.log(`Error! [${text}]`);
    
  113.       }
    
  114.       throw promise;
    
  115.     }
    
  116.   }
    
  117. 
    
  118.   function flushNextRenderIfExpired() {
    
  119.     // This will start rendering the next level of work. If the work hasn't
    
  120.     // expired yet, React will exit without doing anything. If it has expired,
    
  121.     // it will schedule a sync task.
    
  122.     Scheduler.unstable_flushExpired();
    
  123.     // Flush the sync task.
    
  124.     ReactNoop.flushSync();
    
  125.   }
    
  126. 
    
  127.   it('increases priority of updates as time progresses', async () => {
    
  128.     if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  129.       ReactNoop.render(<span prop="done" />);
    
  130.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  131. 
    
  132.       // Nothing has expired yet because time hasn't advanced.
    
  133.       flushNextRenderIfExpired();
    
  134.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  135.       // Advance time a bit, but not enough to expire the low pri update.
    
  136.       ReactNoop.expire(4500);
    
  137.       flushNextRenderIfExpired();
    
  138.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  139.       // Advance by another second. Now the update should expire and flush.
    
  140.       ReactNoop.expire(500);
    
  141.       flushNextRenderIfExpired();
    
  142.       expect(ReactNoop).toMatchRenderedOutput(<span prop="done" />);
    
  143.     } else {
    
  144.       ReactNoop.render(<Text text="Step 1" />);
    
  145.       React.startTransition(() => {
    
  146.         ReactNoop.render(<Text text="Step 2" />);
    
  147.       });
    
  148.       await waitFor(['Step 1']);
    
  149. 
    
  150.       expect(ReactNoop).toMatchRenderedOutput('Step 1');
    
  151. 
    
  152.       // Nothing has expired yet because time hasn't advanced.
    
  153.       await unstable_waitForExpired([]);
    
  154.       expect(ReactNoop).toMatchRenderedOutput('Step 1');
    
  155. 
    
  156.       // Advance time a bit, but not enough to expire the low pri update.
    
  157.       ReactNoop.expire(4500);
    
  158.       await unstable_waitForExpired([]);
    
  159.       expect(ReactNoop).toMatchRenderedOutput('Step 1');
    
  160. 
    
  161.       // Advance by a little bit more. Now the update should expire and flush.
    
  162.       ReactNoop.expire(500);
    
  163.       await unstable_waitForExpired(['Step 2']);
    
  164.       expect(ReactNoop).toMatchRenderedOutput('Step 2');
    
  165.     }
    
  166.   });
    
  167. 
    
  168.   it('two updates of like priority in the same event always flush within the same batch', async () => {
    
  169.     class TextClass extends React.Component {
    
  170.       componentDidMount() {
    
  171.         Scheduler.log(`${this.props.text} [commit]`);
    
  172.       }
    
  173.       componentDidUpdate() {
    
  174.         Scheduler.log(`${this.props.text} [commit]`);
    
  175.       }
    
  176.       render() {
    
  177.         Scheduler.log(`${this.props.text} [render]`);
    
  178.         return <span prop={this.props.text} />;
    
  179.       }
    
  180.     }
    
  181. 
    
  182.     function interrupt() {
    
  183.       ReactNoop.flushSync(() => {
    
  184.         ReactNoop.renderToRootWithID(null, 'other-root');
    
  185.       });
    
  186.     }
    
  187. 
    
  188.     // First, show what happens for updates in two separate events.
    
  189.     // Schedule an update.
    
  190.     React.startTransition(() => {
    
  191.       ReactNoop.render(<TextClass text="A" />);
    
  192.     });
    
  193.     // Advance the timer.
    
  194.     Scheduler.unstable_advanceTime(2000);
    
  195.     // Partially flush the first update, then interrupt it.
    
  196.     await waitFor(['A [render]']);
    
  197.     interrupt();
    
  198. 
    
  199.     // Don't advance time by enough to expire the first update.
    
  200.     assertLog([]);
    
  201.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  202. 
    
  203.     // Schedule another update.
    
  204.     ReactNoop.render(<TextClass text="B" />);
    
  205.     // Both updates are batched
    
  206.     await waitForAll(['B [render]', 'B [commit]']);
    
  207.     expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  208. 
    
  209.     // Now do the same thing again, except this time don't flush any work in
    
  210.     // between the two updates.
    
  211.     ReactNoop.render(<TextClass text="A" />);
    
  212.     Scheduler.unstable_advanceTime(2000);
    
  213.     assertLog([]);
    
  214.     expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  215.     // Schedule another update.
    
  216.     ReactNoop.render(<TextClass text="B" />);
    
  217.     // The updates should flush in the same batch, since as far as the scheduler
    
  218.     // knows, they may have occurred inside the same event.
    
  219.     await waitForAll(['B [render]', 'B [commit]']);
    
  220.   });
    
  221. 
    
  222.   it(
    
  223.     'two updates of like priority in the same event always flush within the ' +
    
  224.       "same batch, even if there's a sync update in between",
    
  225.     async () => {
    
  226.       class TextClass extends React.Component {
    
  227.         componentDidMount() {
    
  228.           Scheduler.log(`${this.props.text} [commit]`);
    
  229.         }
    
  230.         componentDidUpdate() {
    
  231.           Scheduler.log(`${this.props.text} [commit]`);
    
  232.         }
    
  233.         render() {
    
  234.           Scheduler.log(`${this.props.text} [render]`);
    
  235.           return <span prop={this.props.text} />;
    
  236.         }
    
  237.       }
    
  238. 
    
  239.       function interrupt() {
    
  240.         ReactNoop.flushSync(() => {
    
  241.           ReactNoop.renderToRootWithID(null, 'other-root');
    
  242.         });
    
  243.       }
    
  244. 
    
  245.       // First, show what happens for updates in two separate events.
    
  246.       // Schedule an update.
    
  247.       React.startTransition(() => {
    
  248.         ReactNoop.render(<TextClass text="A" />);
    
  249.       });
    
  250. 
    
  251.       // Advance the timer.
    
  252.       Scheduler.unstable_advanceTime(2000);
    
  253.       // Partially flush the first update, then interrupt it.
    
  254.       await waitFor(['A [render]']);
    
  255.       interrupt();
    
  256. 
    
  257.       // Don't advance time by enough to expire the first update.
    
  258.       assertLog([]);
    
  259.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  260. 
    
  261.       // Schedule another update.
    
  262.       ReactNoop.render(<TextClass text="B" />);
    
  263.       // Both updates are batched
    
  264.       await waitForAll(['B [render]', 'B [commit]']);
    
  265.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  266. 
    
  267.       // Now do the same thing again, except this time don't flush any work in
    
  268.       // between the two updates.
    
  269.       ReactNoop.render(<TextClass text="A" />);
    
  270.       Scheduler.unstable_advanceTime(2000);
    
  271.       assertLog([]);
    
  272.       expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
    
  273. 
    
  274.       // Perform some synchronous work. The scheduler must assume we're inside
    
  275.       // the same event.
    
  276.       interrupt();
    
  277. 
    
  278.       // Schedule another update.
    
  279.       ReactNoop.render(<TextClass text="B" />);
    
  280.       // The updates should flush in the same batch, since as far as the scheduler
    
  281.       // knows, they may have occurred inside the same event.
    
  282.       await waitForAll(['B [render]', 'B [commit]']);
    
  283.     },
    
  284.   );
    
  285. 
    
  286.   it('cannot update at the same expiration time that is already rendering', async () => {
    
  287.     const store = {text: 'initial'};
    
  288.     const subscribers = [];
    
  289.     class Connected extends React.Component {
    
  290.       state = {text: store.text};
    
  291.       componentDidMount() {
    
  292.         subscribers.push(this);
    
  293.         Scheduler.log(`${this.state.text} [${this.props.label}] [commit]`);
    
  294.       }
    
  295.       componentDidUpdate() {
    
  296.         Scheduler.log(`${this.state.text} [${this.props.label}] [commit]`);
    
  297.       }
    
  298.       render() {
    
  299.         Scheduler.log(`${this.state.text} [${this.props.label}] [render]`);
    
  300.         return <span prop={this.state.text} />;
    
  301.       }
    
  302.     }
    
  303. 
    
  304.     function App() {
    
  305.       return (
    
  306.         <>
    
  307.           <Connected label="A" />
    
  308.           <Connected label="B" />
    
  309.           <Connected label="C" />
    
  310.           <Connected label="D" />
    
  311.         </>
    
  312.       );
    
  313.     }
    
  314. 
    
  315.     // Initial mount
    
  316.     React.startTransition(() => {
    
  317.       ReactNoop.render(<App />);
    
  318.     });
    
  319. 
    
  320.     await waitForAll([
    
  321.       'initial [A] [render]',
    
  322.       'initial [B] [render]',
    
  323.       'initial [C] [render]',
    
  324.       'initial [D] [render]',
    
  325.       'initial [A] [commit]',
    
  326.       'initial [B] [commit]',
    
  327.       'initial [C] [commit]',
    
  328.       'initial [D] [commit]',
    
  329.     ]);
    
  330. 
    
  331.     // Partial update
    
  332.     React.startTransition(() => {
    
  333.       subscribers.forEach(s => s.setState({text: '1'}));
    
  334.     });
    
  335. 
    
  336.     await waitFor(['1 [A] [render]', '1 [B] [render]']);
    
  337. 
    
  338.     // Before the update can finish, update again. Even though no time has
    
  339.     // advanced, this update should be given a different expiration time than
    
  340.     // the currently rendering one. So, C and D should render with 1, not 2.
    
  341.     React.startTransition(() => {
    
  342.       subscribers.forEach(s => s.setState({text: '2'}));
    
  343.     });
    
  344.     await waitFor(['1 [C] [render]', '1 [D] [render]']);
    
  345.   });
    
  346. 
    
  347.   it('stops yielding if CPU-bound update takes too long to finish', async () => {
    
  348.     const root = ReactNoop.createRoot();
    
  349.     function App() {
    
  350.       return (
    
  351.         <>
    
  352.           <Text text="A" />
    
  353.           <Text text="B" />
    
  354.           <Text text="C" />
    
  355.           <Text text="D" />
    
  356.           <Text text="E" />
    
  357.         </>
    
  358.       );
    
  359.     }
    
  360. 
    
  361.     React.startTransition(() => {
    
  362.       root.render(<App />);
    
  363.     });
    
  364. 
    
  365.     await waitFor(['A']);
    
  366.     await waitFor(['B']);
    
  367.     await waitFor(['C']);
    
  368. 
    
  369.     Scheduler.unstable_advanceTime(10000);
    
  370. 
    
  371.     await unstable_waitForExpired(['D', 'E']);
    
  372.     expect(root).toMatchRenderedOutput('ABCDE');
    
  373.   });
    
  374. 
    
  375.   it('root expiration is measured from the time of the first update', async () => {
    
  376.     Scheduler.unstable_advanceTime(10000);
    
  377. 
    
  378.     const root = ReactNoop.createRoot();
    
  379.     function App() {
    
  380.       return (
    
  381.         <>
    
  382.           <Text text="A" />
    
  383.           <Text text="B" />
    
  384.           <Text text="C" />
    
  385.           <Text text="D" />
    
  386.           <Text text="E" />
    
  387.         </>
    
  388.       );
    
  389.     }
    
  390.     React.startTransition(() => {
    
  391.       root.render(<App />);
    
  392.     });
    
  393. 
    
  394.     await waitFor(['A']);
    
  395.     await waitFor(['B']);
    
  396.     await waitFor(['C']);
    
  397. 
    
  398.     Scheduler.unstable_advanceTime(10000);
    
  399. 
    
  400.     await unstable_waitForExpired(['D', 'E']);
    
  401.     expect(root).toMatchRenderedOutput('ABCDE');
    
  402.   });
    
  403. 
    
  404.   it('should measure expiration times relative to module initialization', async () => {
    
  405.     // Tests an implementation detail where expiration times are computed using
    
  406.     // bitwise operations.
    
  407. 
    
  408.     jest.resetModules();
    
  409.     Scheduler = require('scheduler');
    
  410. 
    
  411.     if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  412.       // Before importing the renderer, advance the current time by a number
    
  413.       // larger than the maximum allowed for bitwise operations.
    
  414.       const maxSigned31BitInt = 1073741823;
    
  415.       Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
    
  416.       // Now import the renderer. On module initialization, it will read the
    
  417.       // current time.
    
  418.       ReactNoop = require('react-noop-renderer');
    
  419.       ReactNoop.render('Hi');
    
  420. 
    
  421.       // The update should not have expired yet.
    
  422.       flushNextRenderIfExpired();
    
  423.       await waitFor([]);
    
  424.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  425.       // Advance the time some more to expire the update.
    
  426.       Scheduler.unstable_advanceTime(10000);
    
  427.       flushNextRenderIfExpired();
    
  428.       await waitFor([]);
    
  429.       expect(ReactNoop).toMatchRenderedOutput('Hi');
    
  430.     } else {
    
  431.       const InternalTestUtils = require('internal-test-utils');
    
  432.       waitFor = InternalTestUtils.waitFor;
    
  433.       assertLog = InternalTestUtils.assertLog;
    
  434.       unstable_waitForExpired = InternalTestUtils.unstable_waitForExpired;
    
  435. 
    
  436.       // Before importing the renderer, advance the current time by a number
    
  437.       // larger than the maximum allowed for bitwise operations.
    
  438.       const maxSigned31BitInt = 1073741823;
    
  439.       Scheduler.unstable_advanceTime(maxSigned31BitInt * 100);
    
  440. 
    
  441.       // Now import the renderer. On module initialization, it will read the
    
  442.       // current time.
    
  443.       ReactNoop = require('react-noop-renderer');
    
  444.       React = require('react');
    
  445. 
    
  446.       ReactNoop.render(<Text text="Step 1" />);
    
  447.       React.startTransition(() => {
    
  448.         ReactNoop.render(<Text text="Step 2" />);
    
  449.       });
    
  450.       await waitFor(['Step 1']);
    
  451. 
    
  452.       // The update should not have expired yet.
    
  453.       await unstable_waitForExpired([]);
    
  454. 
    
  455.       expect(ReactNoop).toMatchRenderedOutput('Step 1');
    
  456. 
    
  457.       // Advance the time some more to expire the update.
    
  458.       Scheduler.unstable_advanceTime(10000);
    
  459.       await unstable_waitForExpired(['Step 2']);
    
  460.       expect(ReactNoop).toMatchRenderedOutput('Step 2');
    
  461.     }
    
  462.   });
    
  463. 
    
  464.   it('should measure callback timeout relative to current time, not start-up time', async () => {
    
  465.     // Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
    
  466.     // The bug wasn't caught by other tests because we use virtual times that
    
  467.     // default to 0, and most tests don't advance time.
    
  468. 
    
  469.     // Before scheduling an update, advance the current time.
    
  470.     Scheduler.unstable_advanceTime(10000);
    
  471. 
    
  472.     React.startTransition(() => {
    
  473.       ReactNoop.render('Hi');
    
  474.     });
    
  475. 
    
  476.     await unstable_waitForExpired([]);
    
  477.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  478. 
    
  479.     // Advancing by ~5 seconds should be sufficient to expire the update. (I
    
  480.     // used a slightly larger number to allow for possible rounding.)
    
  481.     Scheduler.unstable_advanceTime(6000);
    
  482.     await unstable_waitForExpired([]);
    
  483.     expect(ReactNoop).toMatchRenderedOutput('Hi');
    
  484.   });
    
  485. 
    
  486.   it('prevents starvation by sync updates by disabling time slicing if too much time has elapsed', async () => {
    
  487.     let updateSyncPri;
    
  488.     let updateNormalPri;
    
  489.     function App() {
    
  490.       const [highPri, setHighPri] = useState(0);
    
  491.       const [normalPri, setNormalPri] = useState(0);
    
  492.       updateSyncPri = () => {
    
  493.         ReactNoop.flushSync(() => {
    
  494.           setHighPri(n => n + 1);
    
  495.         });
    
  496.       };
    
  497.       updateNormalPri = () => setNormalPri(n => n + 1);
    
  498.       return (
    
  499.         <>
    
  500.           <Text text={'Sync pri: ' + highPri} />
    
  501.           {', '}
    
  502.           <Text text={'Normal pri: ' + normalPri} />
    
  503.         </>
    
  504.       );
    
  505.     }
    
  506. 
    
  507.     const root = ReactNoop.createRoot();
    
  508.     await act(() => {
    
  509.       root.render(<App />);
    
  510.     });
    
  511.     assertLog(['Sync pri: 0', 'Normal pri: 0']);
    
  512.     expect(root).toMatchRenderedOutput('Sync pri: 0, Normal pri: 0');
    
  513. 
    
  514.     // First demonstrate what happens when there's no starvation
    
  515.     await act(async () => {
    
  516.       React.startTransition(() => {
    
  517.         updateNormalPri();
    
  518.       });
    
  519.       await waitFor(['Sync pri: 0']);
    
  520.       updateSyncPri();
    
  521.       assertLog(['Sync pri: 1', 'Normal pri: 0']);
    
  522. 
    
  523.       // The remaining work hasn't expired, so the render phase is time sliced.
    
  524.       // In other words, we can flush just the first child without flushing
    
  525.       // the rest.
    
  526.       //
    
  527.       // Yield right after first child.
    
  528.       await waitFor(['Sync pri: 1']);
    
  529.       // Now do the rest.
    
  530.       await waitForAll(['Normal pri: 1']);
    
  531.     });
    
  532.     expect(root).toMatchRenderedOutput('Sync pri: 1, Normal pri: 1');
    
  533. 
    
  534.     // Do the same thing, but starve the first update
    
  535.     await act(async () => {
    
  536.       React.startTransition(() => {
    
  537.         updateNormalPri();
    
  538.       });
    
  539.       await waitFor(['Sync pri: 1']);
    
  540. 
    
  541.       // This time, a lot of time has elapsed since the normal pri update
    
  542.       // started rendering. (This should advance time by some number that's
    
  543.       // definitely bigger than the constant heuristic we use to detect
    
  544.       // starvation of normal priority updates.)
    
  545.       Scheduler.unstable_advanceTime(10000);
    
  546. 
    
  547.       updateSyncPri();
    
  548.       assertLog(['Sync pri: 2', 'Normal pri: 1']);
    
  549. 
    
  550.       // The remaining work _has_ expired, so the render phase is _not_ time
    
  551.       // sliced. Attempting to flush just the first child also flushes the rest.
    
  552.       await waitFor(['Sync pri: 2'], {
    
  553.         additionalLogsAfterAttemptingToYield: ['Normal pri: 2'],
    
  554.       });
    
  555.     });
    
  556.     expect(root).toMatchRenderedOutput('Sync pri: 2, Normal pri: 2');
    
  557.   });
    
  558. 
    
  559.   it('idle work never expires', async () => {
    
  560.     let updateSyncPri;
    
  561.     let updateIdlePri;
    
  562.     function App() {
    
  563.       const [syncPri, setSyncPri] = useState(0);
    
  564.       const [highPri, setIdlePri] = useState(0);
    
  565.       updateSyncPri = () => ReactNoop.flushSync(() => setSyncPri(n => n + 1));
    
  566.       updateIdlePri = () =>
    
  567.         ReactNoop.idleUpdates(() => {
    
  568.           setIdlePri(n => n + 1);
    
  569.         });
    
  570.       return (
    
  571.         <>
    
  572.           <Text text={'Sync pri: ' + syncPri} />
    
  573.           {', '}
    
  574.           <Text text={'Idle pri: ' + highPri} />
    
  575.         </>
    
  576.       );
    
  577.     }
    
  578. 
    
  579.     const root = ReactNoop.createRoot();
    
  580.     await act(() => {
    
  581.       root.render(<App />);
    
  582.     });
    
  583.     assertLog(['Sync pri: 0', 'Idle pri: 0']);
    
  584.     expect(root).toMatchRenderedOutput('Sync pri: 0, Idle pri: 0');
    
  585. 
    
  586.     // First demonstrate what happens when there's no starvation
    
  587.     await act(async () => {
    
  588.       updateIdlePri();
    
  589.       await waitFor(['Sync pri: 0']);
    
  590.       updateSyncPri();
    
  591.     });
    
  592.     // Same thing should happen as last time
    
  593.     assertLog([
    
  594.       // Interrupt idle update to render sync update
    
  595.       'Sync pri: 1',
    
  596.       'Idle pri: 0',
    
  597.       // Now render idle
    
  598.       'Sync pri: 1',
    
  599.       'Idle pri: 1',
    
  600.     ]);
    
  601.     expect(root).toMatchRenderedOutput('Sync pri: 1, Idle pri: 1');
    
  602. 
    
  603.     // Do the same thing, but starve the first update
    
  604.     await act(async () => {
    
  605.       updateIdlePri();
    
  606.       await waitFor(['Sync pri: 1']);
    
  607. 
    
  608.       // Advance a ridiculously large amount of time to demonstrate that the
    
  609.       // idle work never expires
    
  610.       Scheduler.unstable_advanceTime(100000);
    
  611. 
    
  612.       updateSyncPri();
    
  613.     });
    
  614.     assertLog([
    
  615.       // Interrupt idle update to render sync update
    
  616.       'Sync pri: 2',
    
  617.       'Idle pri: 1',
    
  618.       // Now render idle
    
  619.       'Sync pri: 2',
    
  620.       'Idle pri: 2',
    
  621.     ]);
    
  622.     expect(root).toMatchRenderedOutput('Sync pri: 2, Idle pri: 2');
    
  623.   });
    
  624. 
    
  625.   it('when multiple lanes expire, we can finish the in-progress one without including the others', async () => {
    
  626.     let setA;
    
  627.     let setB;
    
  628.     function App() {
    
  629.       const [a, _setA] = useState(0);
    
  630.       const [b, _setB] = useState(0);
    
  631.       setA = _setA;
    
  632.       setB = _setB;
    
  633.       return (
    
  634.         <>
    
  635.           <Text text={'A' + a} />
    
  636.           <Text text={'B' + b} />
    
  637.           <Text text="C" />
    
  638.         </>
    
  639.       );
    
  640.     }
    
  641. 
    
  642.     const root = ReactNoop.createRoot();
    
  643.     await act(() => {
    
  644.       root.render(<App />);
    
  645.     });
    
  646.     assertLog(['A0', 'B0', 'C']);
    
  647.     expect(root).toMatchRenderedOutput('A0B0C');
    
  648. 
    
  649.     await act(async () => {
    
  650.       startTransition(() => {
    
  651.         setA(1);
    
  652.       });
    
  653.       await waitFor(['A1']);
    
  654.       startTransition(() => {
    
  655.         setB(1);
    
  656.       });
    
  657.       await waitFor(['B0']);
    
  658. 
    
  659.       // Expire both the transitions
    
  660.       Scheduler.unstable_advanceTime(10000);
    
  661.       // Both transitions have expired, but since they aren't related
    
  662.       // (entangled), we should be able to finish the in-progress transition
    
  663.       // without also including the next one.
    
  664.       await waitFor([], {
    
  665.         additionalLogsAfterAttemptingToYield: ['C'],
    
  666.       });
    
  667.       expect(root).toMatchRenderedOutput('A1B0C');
    
  668. 
    
  669.       // The next transition also finishes without yielding.
    
  670.       await waitFor(['A1'], {
    
  671.         additionalLogsAfterAttemptingToYield: ['B1', 'C'],
    
  672.       });
    
  673.       expect(root).toMatchRenderedOutput('A1B1C');
    
  674.     });
    
  675.   });
    
  676. 
    
  677.   it('updates do not expire while they are IO-bound', async () => {
    
  678.     const {Suspense} = React;
    
  679. 
    
  680.     function App({step}) {
    
  681.       return (
    
  682.         <Suspense fallback={<Text text="Loading..." />}>
    
  683.           <AsyncText text={'A' + step} />
    
  684.           <Text text="B" />
    
  685.           <Text text="C" />
    
  686.         </Suspense>
    
  687.       );
    
  688.     }
    
  689. 
    
  690.     const root = ReactNoop.createRoot();
    
  691.     await act(async () => {
    
  692.       await resolveText('A0');
    
  693.       root.render(<App step={0} />);
    
  694.     });
    
  695.     assertLog(['A0', 'B', 'C']);
    
  696.     expect(root).toMatchRenderedOutput('A0BC');
    
  697. 
    
  698.     await act(async () => {
    
  699.       React.startTransition(() => {
    
  700.         root.render(<App step={1} />);
    
  701.       });
    
  702.       await waitForAll(['Suspend! [A1]', 'Loading...']);
    
  703. 
    
  704.       // Lots of time elapses before the promise resolves
    
  705.       Scheduler.unstable_advanceTime(10000);
    
  706.       await resolveText('A1');
    
  707.       assertLog(['Promise resolved [A1]']);
    
  708. 
    
  709.       await waitFor(['A1']);
    
  710.       expect(root).toMatchRenderedOutput('A0BC');
    
  711. 
    
  712.       // Lots more time elapses. We're CPU-bound now, so we should treat this
    
  713.       // as starvation.
    
  714.       Scheduler.unstable_advanceTime(10000);
    
  715. 
    
  716.       // The rest of the update finishes without yielding.
    
  717.       await waitFor([], {
    
  718.         additionalLogsAfterAttemptingToYield: ['B', 'C'],
    
  719.       });
    
  720.     });
    
  721.   });
    
  722. 
    
  723.   it('flushSync should not affect expired work', async () => {
    
  724.     let setA;
    
  725.     let setB;
    
  726.     function App() {
    
  727.       const [a, _setA] = useState(0);
    
  728.       const [b, _setB] = useState(0);
    
  729.       setA = _setA;
    
  730.       setB = _setB;
    
  731.       return (
    
  732.         <>
    
  733.           <Text text={'A' + a} />
    
  734.           <Text text={'B' + b} />
    
  735.         </>
    
  736.       );
    
  737.     }
    
  738. 
    
  739.     const root = ReactNoop.createRoot();
    
  740.     await act(() => {
    
  741.       root.render(<App />);
    
  742.     });
    
  743.     assertLog(['A0', 'B0']);
    
  744. 
    
  745.     await act(async () => {
    
  746.       startTransition(() => {
    
  747.         setA(1);
    
  748.       });
    
  749.       await waitFor(['A1']);
    
  750. 
    
  751.       // Expire the in-progress update
    
  752.       Scheduler.unstable_advanceTime(10000);
    
  753. 
    
  754.       ReactNoop.flushSync(() => {
    
  755.         setB(1);
    
  756.       });
    
  757.       assertLog(['A0', 'B1']);
    
  758. 
    
  759.       // Now flush the original update. Because it expired, it should finish
    
  760.       // without yielding.
    
  761.       await waitFor(['A1'], {
    
  762.         additionalLogsAfterAttemptingToYield: ['B1'],
    
  763.       });
    
  764.     });
    
  765.   });
    
  766. 
    
  767.   it('passive effects of expired update flush after paint', async () => {
    
  768.     function App({step}) {
    
  769.       useEffect(() => {
    
  770.         Scheduler.log('Effect: ' + step);
    
  771.       }, [step]);
    
  772.       return (
    
  773.         <>
    
  774.           <Text text={'A' + step} />
    
  775.           <Text text={'B' + step} />
    
  776.           <Text text={'C' + step} />
    
  777.         </>
    
  778.       );
    
  779.     }
    
  780. 
    
  781.     const root = ReactNoop.createRoot();
    
  782.     await act(() => {
    
  783.       root.render(<App step={0} />);
    
  784.     });
    
  785.     assertLog(['A0', 'B0', 'C0', 'Effect: 0']);
    
  786.     expect(root).toMatchRenderedOutput('A0B0C0');
    
  787. 
    
  788.     await act(async () => {
    
  789.       startTransition(() => {
    
  790.         root.render(<App step={1} />);
    
  791.       });
    
  792.       await waitFor(['A1']);
    
  793. 
    
  794.       // Expire the update
    
  795.       Scheduler.unstable_advanceTime(10000);
    
  796. 
    
  797.       // The update finishes without yielding. But it does not flush the effect.
    
  798.       await waitFor(['B1'], {
    
  799.         additionalLogsAfterAttemptingToYield: ['C1'],
    
  800.       });
    
  801.     });
    
  802.     // The effect flushes after paint.
    
  803.     assertLog(['Effect: 1']);
    
  804.   });
    
  805. });