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 ContinuousEventPriority;
    
  17. let act;
    
  18. let waitForAll;
    
  19. let waitFor;
    
  20. let assertLog;
    
  21. 
    
  22. describe('ReactIncrementalUpdates', () => {
    
  23.   beforeEach(() => {
    
  24.     jest.resetModules();
    
  25. 
    
  26.     React = require('react');
    
  27.     ReactNoop = require('react-noop-renderer');
    
  28.     Scheduler = require('scheduler');
    
  29.     act = require('internal-test-utils').act;
    
  30.     ContinuousEventPriority =
    
  31.       require('react-reconciler/constants').ContinuousEventPriority;
    
  32. 
    
  33.     const InternalTestUtils = require('internal-test-utils');
    
  34.     waitForAll = InternalTestUtils.waitForAll;
    
  35.     waitFor = InternalTestUtils.waitFor;
    
  36.     assertLog = InternalTestUtils.assertLog;
    
  37.   });
    
  38. 
    
  39.   function Text({text}) {
    
  40.     Scheduler.log(text);
    
  41.     return text;
    
  42.   }
    
  43. 
    
  44.   it('applies updates in order of priority', async () => {
    
  45.     let state;
    
  46.     class Foo extends React.Component {
    
  47.       state = {};
    
  48.       componentDidMount() {
    
  49.         Scheduler.log('commit');
    
  50.         React.startTransition(() => {
    
  51.           // Has low priority
    
  52.           this.setState({b: 'b'});
    
  53.           this.setState({c: 'c'});
    
  54.         });
    
  55.         // Has Task priority
    
  56.         this.setState({a: 'a'});
    
  57.       }
    
  58.       render() {
    
  59.         state = this.state;
    
  60.         return <div />;
    
  61.       }
    
  62.     }
    
  63. 
    
  64.     ReactNoop.render(<Foo />);
    
  65.     await waitFor(['commit']);
    
  66. 
    
  67.     expect(state).toEqual({a: 'a'});
    
  68.     await waitForAll([]);
    
  69.     expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
    
  70.   });
    
  71. 
    
  72.   it('applies updates with equal priority in insertion order', async () => {
    
  73.     let state;
    
  74.     class Foo extends React.Component {
    
  75.       state = {};
    
  76.       componentDidMount() {
    
  77.         // All have Task priority
    
  78.         this.setState({a: 'a'});
    
  79.         this.setState({b: 'b'});
    
  80.         this.setState({c: 'c'});
    
  81.       }
    
  82.       render() {
    
  83.         state = this.state;
    
  84.         return <div />;
    
  85.       }
    
  86.     }
    
  87. 
    
  88.     ReactNoop.render(<Foo />);
    
  89.     await waitForAll([]);
    
  90.     expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
    
  91.   });
    
  92. 
    
  93.   it('only drops updates with equal or lesser priority when replaceState is called', async () => {
    
  94.     let instance;
    
  95.     class Foo extends React.Component {
    
  96.       state = {};
    
  97.       componentDidMount() {
    
  98.         Scheduler.log('componentDidMount');
    
  99.       }
    
  100.       componentDidUpdate() {
    
  101.         Scheduler.log('componentDidUpdate');
    
  102.       }
    
  103.       render() {
    
  104.         Scheduler.log('render');
    
  105.         instance = this;
    
  106.         return <div />;
    
  107.       }
    
  108.     }
    
  109. 
    
  110.     ReactNoop.render(<Foo />);
    
  111.     await waitForAll(['render', 'componentDidMount']);
    
  112. 
    
  113.     ReactNoop.flushSync(() => {
    
  114.       React.startTransition(() => {
    
  115.         instance.setState({x: 'x'});
    
  116.         instance.setState({y: 'y'});
    
  117.       });
    
  118.       instance.setState({a: 'a'});
    
  119.       instance.setState({b: 'b'});
    
  120.       React.startTransition(() => {
    
  121.         instance.updater.enqueueReplaceState(instance, {c: 'c'});
    
  122.         instance.setState({d: 'd'});
    
  123.       });
    
  124.     });
    
  125. 
    
  126.     // Even though a replaceState has been already scheduled, it hasn't been
    
  127.     // flushed yet because it has async priority.
    
  128.     expect(instance.state).toEqual({a: 'a', b: 'b'});
    
  129.     assertLog(['render', 'componentDidUpdate']);
    
  130. 
    
  131.     await waitForAll(['render', 'componentDidUpdate']);
    
  132.     // Now the rest of the updates are flushed, including the replaceState.
    
  133.     expect(instance.state).toEqual({c: 'c', d: 'd'});
    
  134.   });
    
  135. 
    
  136.   it('can abort an update, schedule additional updates, and resume', async () => {
    
  137.     let instance;
    
  138.     class Foo extends React.Component {
    
  139.       state = {};
    
  140.       render() {
    
  141.         instance = this;
    
  142.         return <span prop={Object.keys(this.state).sort().join('')} />;
    
  143.       }
    
  144.     }
    
  145. 
    
  146.     ReactNoop.render(<Foo />);
    
  147.     await waitForAll([]);
    
  148. 
    
  149.     function createUpdate(letter) {
    
  150.       return () => {
    
  151.         Scheduler.log(letter);
    
  152.         return {
    
  153.           [letter]: letter,
    
  154.         };
    
  155.       };
    
  156.     }
    
  157. 
    
  158.     // Schedule some async updates
    
  159.     if (
    
  160.       gate(
    
  161.         flags =>
    
  162.           !flags.forceConcurrentByDefaultForTesting ||
    
  163.           flags.enableUnifiedSyncLane,
    
  164.       )
    
  165.     ) {
    
  166.       React.startTransition(() => {
    
  167.         instance.setState(createUpdate('a'));
    
  168.         instance.setState(createUpdate('b'));
    
  169.         instance.setState(createUpdate('c'));
    
  170.       });
    
  171.     } else {
    
  172.       instance.setState(createUpdate('a'));
    
  173.       instance.setState(createUpdate('b'));
    
  174.       instance.setState(createUpdate('c'));
    
  175.     }
    
  176. 
    
  177.     // Begin the updates but don't flush them yet
    
  178.     await waitFor(['a', 'b', 'c']);
    
  179.     expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
    
  180. 
    
  181.     // Schedule some more updates at different priorities
    
  182.     instance.setState(createUpdate('d'));
    
  183.     ReactNoop.flushSync(() => {
    
  184.       instance.setState(createUpdate('e'));
    
  185.       instance.setState(createUpdate('f'));
    
  186.     });
    
  187.     React.startTransition(() => {
    
  188.       instance.setState(createUpdate('g'));
    
  189.     });
    
  190. 
    
  191.     // The sync updates should have flushed, but not the async ones.
    
  192.     if (
    
  193.       gate(
    
  194.         flags =>
    
  195.           !flags.forceConcurrentByDefaultForTesting &&
    
  196.           flags.enableUnifiedSyncLane,
    
  197.       )
    
  198.     ) {
    
  199.       assertLog(['d', 'e', 'f']);
    
  200.       expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />);
    
  201.     } else {
    
  202.       // Update d was dropped and replaced by e.
    
  203.       assertLog(['e', 'f']);
    
  204.       expect(ReactNoop).toMatchRenderedOutput(<span prop="ef" />);
    
  205.     }
    
  206. 
    
  207.     // Now flush the remaining work. Even though e and f were already processed,
    
  208.     // they should be processed again, to ensure that the terminal state
    
  209.     // is deterministic.
    
  210.     if (
    
  211.       gate(
    
  212.         flags =>
    
  213.           !flags.forceConcurrentByDefaultForTesting &&
    
  214.           !flags.enableUnifiedSyncLane,
    
  215.       )
    
  216.     ) {
    
  217.       await waitForAll([
    
  218.         // Since 'g' is in a transition, we'll process 'd' separately first.
    
  219.         // That causes us to process 'd' with 'e' and 'f' rebased.
    
  220.         'd',
    
  221.         'e',
    
  222.         'f',
    
  223.         // Then we'll re-process everything for 'g'.
    
  224.         'a',
    
  225.         'b',
    
  226.         'c',
    
  227.         'd',
    
  228.         'e',
    
  229.         'f',
    
  230.         'g',
    
  231.       ]);
    
  232.     } else {
    
  233.       await waitForAll([
    
  234.         // Then we'll re-process everything for 'g'.
    
  235.         'a',
    
  236.         'b',
    
  237.         'c',
    
  238.         'd',
    
  239.         'e',
    
  240.         'f',
    
  241.         'g',
    
  242.       ]);
    
  243.     }
    
  244.     expect(ReactNoop).toMatchRenderedOutput(<span prop="abcdefg" />);
    
  245.   });
    
  246. 
    
  247.   it('can abort an update, schedule a replaceState, and resume', async () => {
    
  248.     let instance;
    
  249.     class Foo extends React.Component {
    
  250.       state = {};
    
  251.       render() {
    
  252.         instance = this;
    
  253.         return <span prop={Object.keys(this.state).sort().join('')} />;
    
  254.       }
    
  255.     }
    
  256. 
    
  257.     ReactNoop.render(<Foo />);
    
  258.     await waitForAll([]);
    
  259. 
    
  260.     function createUpdate(letter) {
    
  261.       return () => {
    
  262.         Scheduler.log(letter);
    
  263.         return {
    
  264.           [letter]: letter,
    
  265.         };
    
  266.       };
    
  267.     }
    
  268. 
    
  269.     // Schedule some async updates
    
  270.     if (
    
  271.       gate(
    
  272.         flags =>
    
  273.           !flags.forceConcurrentByDefaultForTesting ||
    
  274.           flags.enableUnifiedSyncLane,
    
  275.       )
    
  276.     ) {
    
  277.       React.startTransition(() => {
    
  278.         instance.setState(createUpdate('a'));
    
  279.         instance.setState(createUpdate('b'));
    
  280.         instance.setState(createUpdate('c'));
    
  281.       });
    
  282.     } else {
    
  283.       instance.setState(createUpdate('a'));
    
  284.       instance.setState(createUpdate('b'));
    
  285.       instance.setState(createUpdate('c'));
    
  286.     }
    
  287. 
    
  288.     // Begin the updates but don't flush them yet
    
  289.     await waitFor(['a', 'b', 'c']);
    
  290.     expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
    
  291. 
    
  292.     // Schedule some more updates at different priorities
    
  293.     instance.setState(createUpdate('d'));
    
  294. 
    
  295.     ReactNoop.flushSync(() => {
    
  296.       instance.setState(createUpdate('e'));
    
  297.       // No longer a public API, but we can test that it works internally by
    
  298.       // reaching into the updater.
    
  299.       instance.updater.enqueueReplaceState(instance, createUpdate('f'));
    
  300.     });
    
  301.     React.startTransition(() => {
    
  302.       instance.setState(createUpdate('g'));
    
  303.     });
    
  304. 
    
  305.     // The sync updates should have flushed, but not the async ones.
    
  306.     if (
    
  307.       gate(
    
  308.         flags =>
    
  309.           !flags.forceConcurrentByDefaultForTesting &&
    
  310.           flags.enableUnifiedSyncLane,
    
  311.       )
    
  312.     ) {
    
  313.       assertLog(['d', 'e', 'f']);
    
  314.     } else {
    
  315.       // Update d was dropped and replaced by e.
    
  316.       assertLog(['e', 'f']);
    
  317.     }
    
  318.     expect(ReactNoop).toMatchRenderedOutput(<span prop="f" />);
    
  319. 
    
  320.     // Now flush the remaining work. Even though e and f were already processed,
    
  321.     // they should be processed again, to ensure that the terminal state
    
  322.     // is deterministic.
    
  323.     if (
    
  324.       gate(
    
  325.         flags =>
    
  326.           !flags.forceConcurrentByDefaultForTesting &&
    
  327.           !flags.enableUnifiedSyncLane,
    
  328.       )
    
  329.     ) {
    
  330.       await waitForAll([
    
  331.         // Since 'g' is in a transition, we'll process 'd' separately first.
    
  332.         // That causes us to process 'd' with 'e' and 'f' rebased.
    
  333.         'd',
    
  334.         'e',
    
  335.         'f',
    
  336.         // Then we'll re-process everything for 'g'.
    
  337.         'a',
    
  338.         'b',
    
  339.         'c',
    
  340.         'd',
    
  341.         'e',
    
  342.         'f',
    
  343.         'g',
    
  344.       ]);
    
  345.     } else {
    
  346.       await waitForAll([
    
  347.         // Then we'll re-process everything for 'g'.
    
  348.         'a',
    
  349.         'b',
    
  350.         'c',
    
  351.         'd',
    
  352.         'e',
    
  353.         'f',
    
  354.         'g',
    
  355.       ]);
    
  356.     }
    
  357.     expect(ReactNoop).toMatchRenderedOutput(<span prop="fg" />);
    
  358.   });
    
  359. 
    
  360.   it('passes accumulation of previous updates to replaceState updater function', async () => {
    
  361.     let instance;
    
  362.     class Foo extends React.Component {
    
  363.       state = {};
    
  364.       render() {
    
  365.         instance = this;
    
  366.         return <span />;
    
  367.       }
    
  368.     }
    
  369.     ReactNoop.render(<Foo />);
    
  370.     await waitForAll([]);
    
  371. 
    
  372.     instance.setState({a: 'a'});
    
  373.     instance.setState({b: 'b'});
    
  374.     // No longer a public API, but we can test that it works internally by
    
  375.     // reaching into the updater.
    
  376.     instance.updater.enqueueReplaceState(instance, previousState => ({
    
  377.       previousState,
    
  378.     }));
    
  379.     await waitForAll([]);
    
  380.     expect(instance.state).toEqual({previousState: {a: 'a', b: 'b'}});
    
  381.   });
    
  382. 
    
  383.   it('does not call callbacks that are scheduled by another callback until a later commit', async () => {
    
  384.     class Foo extends React.Component {
    
  385.       state = {};
    
  386.       componentDidMount() {
    
  387.         Scheduler.log('did mount');
    
  388.         this.setState({a: 'a'}, () => {
    
  389.           Scheduler.log('callback a');
    
  390.           this.setState({b: 'b'}, () => {
    
  391.             Scheduler.log('callback b');
    
  392.           });
    
  393.         });
    
  394.       }
    
  395.       render() {
    
  396.         Scheduler.log('render');
    
  397.         return <div />;
    
  398.       }
    
  399.     }
    
  400. 
    
  401.     ReactNoop.render(<Foo />);
    
  402.     await waitForAll([
    
  403.       'render',
    
  404.       'did mount',
    
  405.       'render',
    
  406.       'callback a',
    
  407.       'render',
    
  408.       'callback b',
    
  409.     ]);
    
  410.   });
    
  411. 
    
  412.   it('gives setState during reconciliation the same priority as whatever level is currently reconciling', async () => {
    
  413.     let instance;
    
  414. 
    
  415.     class Foo extends React.Component {
    
  416.       state = {};
    
  417.       UNSAFE_componentWillReceiveProps() {
    
  418.         Scheduler.log('componentWillReceiveProps');
    
  419.         this.setState({b: 'b'});
    
  420.       }
    
  421.       render() {
    
  422.         Scheduler.log('render');
    
  423.         instance = this;
    
  424.         return <div />;
    
  425.       }
    
  426.     }
    
  427.     ReactNoop.render(<Foo />);
    
  428.     await waitForAll(['render']);
    
  429. 
    
  430.     ReactNoop.flushSync(() => {
    
  431.       instance.setState({a: 'a'});
    
  432. 
    
  433.       ReactNoop.render(<Foo />); // Trigger componentWillReceiveProps
    
  434.     });
    
  435. 
    
  436.     expect(instance.state).toEqual({a: 'a', b: 'b'});
    
  437. 
    
  438.     assertLog(['componentWillReceiveProps', 'render']);
    
  439.   });
    
  440. 
    
  441.   it('updates triggered from inside a class setState updater', async () => {
    
  442.     let instance;
    
  443.     class Foo extends React.Component {
    
  444.       state = {};
    
  445.       render() {
    
  446.         Scheduler.log('render');
    
  447.         instance = this;
    
  448.         return <div />;
    
  449.       }
    
  450.     }
    
  451. 
    
  452.     ReactNoop.render(<Foo />);
    
  453.     await waitForAll([
    
  454.       // Initial render
    
  455.       'render',
    
  456.     ]);
    
  457. 
    
  458.     instance.setState(function a() {
    
  459.       Scheduler.log('setState updater');
    
  460.       this.setState({b: 'b'});
    
  461.       return {a: 'a'};
    
  462.     });
    
  463. 
    
  464.     await expect(
    
  465.       async () =>
    
  466.         await waitForAll([
    
  467.           'setState updater',
    
  468.           // Updates in the render phase receive the currently rendering
    
  469.           // lane, so the update flushes immediately in the same render.
    
  470.           'render',
    
  471.         ]),
    
  472.     ).toErrorDev(
    
  473.       'An update (setState, replaceState, or forceUpdate) was scheduled ' +
    
  474.         'from inside an update function. Update functions should be pure, ' +
    
  475.         'with zero side-effects. Consider using componentDidUpdate or a ' +
    
  476.         'callback.\n\nPlease update the following component: Foo',
    
  477.     );
    
  478.     expect(instance.state).toEqual({a: 'a', b: 'b'});
    
  479. 
    
  480.     // Test deduplication (no additional warnings expected)
    
  481.     instance.setState(function a() {
    
  482.       this.setState({a: 'a'});
    
  483.       return {b: 'b'};
    
  484.     });
    
  485.     await waitForAll(
    
  486.       gate(flags =>
    
  487.         // Updates in the render phase receive the currently rendering
    
  488.         // lane, so the update flushes immediately in the same render.
    
  489.         ['render'],
    
  490.       ),
    
  491.     );
    
  492.   });
    
  493. 
    
  494.   it('getDerivedStateFromProps should update base state of updateQueue (based on product bug)', () => {
    
  495.     // Based on real-world bug.
    
  496. 
    
  497.     let foo;
    
  498.     class Foo extends React.Component {
    
  499.       state = {value: 'initial state'};
    
  500.       static getDerivedStateFromProps() {
    
  501.         return {value: 'derived state'};
    
  502.       }
    
  503.       render() {
    
  504.         foo = this;
    
  505.         return (
    
  506.           <>
    
  507.             <span prop={this.state.value} />
    
  508.             <Bar />
    
  509.           </>
    
  510.         );
    
  511.       }
    
  512.     }
    
  513. 
    
  514.     let bar;
    
  515.     class Bar extends React.Component {
    
  516.       render() {
    
  517.         bar = this;
    
  518.         return null;
    
  519.       }
    
  520.     }
    
  521. 
    
  522.     ReactNoop.flushSync(() => {
    
  523.       ReactNoop.render(<Foo />);
    
  524.     });
    
  525.     expect(ReactNoop).toMatchRenderedOutput(<span prop="derived state" />);
    
  526. 
    
  527.     ReactNoop.flushSync(() => {
    
  528.       // Triggers getDerivedStateFromProps again
    
  529.       ReactNoop.render(<Foo />);
    
  530.       // The noop callback is needed to trigger the specific internal path that
    
  531.       // led to this bug. Removing it causes it to "accidentally" work.
    
  532.       foo.setState({value: 'update state'}, function noop() {});
    
  533.     });
    
  534.     expect(ReactNoop).toMatchRenderedOutput(<span prop="derived state" />);
    
  535. 
    
  536.     ReactNoop.flushSync(() => {
    
  537.       bar.setState({});
    
  538.     });
    
  539.     expect(ReactNoop).toMatchRenderedOutput(<span prop="derived state" />);
    
  540.   });
    
  541. 
    
  542.   it('regression: does not expire soon due to layout effects in the last batch', async () => {
    
  543.     const {useState, useLayoutEffect} = React;
    
  544. 
    
  545.     let setCount;
    
  546.     function App() {
    
  547.       const [count, _setCount] = useState(0);
    
  548.       setCount = _setCount;
    
  549.       Scheduler.log('Render: ' + count);
    
  550.       useLayoutEffect(() => {
    
  551.         setCount(1);
    
  552.         Scheduler.log('Commit: ' + count);
    
  553.       }, []);
    
  554.       return <Text text="Child" />;
    
  555.     }
    
  556. 
    
  557.     await act(async () => {
    
  558.       React.startTransition(() => {
    
  559.         ReactNoop.render(<App />);
    
  560.       });
    
  561.       assertLog([]);
    
  562.       await waitForAll([
    
  563.         'Render: 0',
    
  564.         'Child',
    
  565.         'Commit: 0',
    
  566.         'Render: 1',
    
  567.         'Child',
    
  568.       ]);
    
  569. 
    
  570.       Scheduler.unstable_advanceTime(10000);
    
  571.       React.startTransition(() => {
    
  572.         setCount(2);
    
  573.       });
    
  574.       // The transition should not have expired, so we should be able to
    
  575.       // partially render it.
    
  576.       await waitFor(['Render: 2']);
    
  577.       // Now do the rest
    
  578.       await waitForAll(['Child']);
    
  579.     });
    
  580.   });
    
  581. 
    
  582.   it('regression: does not expire soon due to previous flushSync', async () => {
    
  583.     ReactNoop.flushSync(() => {
    
  584.       ReactNoop.render(<Text text="A" />);
    
  585.     });
    
  586.     assertLog(['A']);
    
  587. 
    
  588.     Scheduler.unstable_advanceTime(10000);
    
  589. 
    
  590.     React.startTransition(() => {
    
  591.       ReactNoop.render(
    
  592.         <>
    
  593.           <Text text="A" />
    
  594.           <Text text="B" />
    
  595.           <Text text="C" />
    
  596.           <Text text="D" />
    
  597.         </>,
    
  598.       );
    
  599.     });
    
  600.     // The transition should not have expired, so we should be able to
    
  601.     // partially render it.
    
  602.     await waitFor(['A']);
    
  603.     await waitFor(['B']);
    
  604.     await waitForAll(['C', 'D']);
    
  605.   });
    
  606. 
    
  607.   it('regression: does not expire soon due to previous expired work', async () => {
    
  608.     React.startTransition(() => {
    
  609.       ReactNoop.render(
    
  610.         <>
    
  611.           <Text text="A" />
    
  612.           <Text text="B" />
    
  613.           <Text text="C" />
    
  614.           <Text text="D" />
    
  615.         </>,
    
  616.       );
    
  617.     });
    
  618. 
    
  619.     await waitFor(['A']);
    
  620.     // This will expire the rest of the update
    
  621.     Scheduler.unstable_advanceTime(10000);
    
  622.     await waitFor(['B'], {
    
  623.       additionalLogsAfterAttemptingToYield: ['C', 'D'],
    
  624.     });
    
  625. 
    
  626.     Scheduler.unstable_advanceTime(10000);
    
  627. 
    
  628.     // Now do another transition. This one should not expire.
    
  629.     React.startTransition(() => {
    
  630.       ReactNoop.render(
    
  631.         <>
    
  632.           <Text text="A" />
    
  633.           <Text text="B" />
    
  634.           <Text text="C" />
    
  635.           <Text text="D" />
    
  636.         </>,
    
  637.       );
    
  638.     });
    
  639. 
    
  640.     // The transition should not have expired, so we should be able to
    
  641.     // partially render it.
    
  642.     await waitFor(['A']);
    
  643.     await waitFor(['B']);
    
  644.     await waitForAll(['C', 'D']);
    
  645.   });
    
  646. 
    
  647.   it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
    
  648.     const {useState, useLayoutEffect} = React;
    
  649. 
    
  650.     let pushToLog;
    
  651.     function App() {
    
  652.       const [log, setLog] = useState('');
    
  653.       pushToLog = msg => {
    
  654.         setLog(prevLog => prevLog + msg);
    
  655.       };
    
  656. 
    
  657.       useLayoutEffect(() => {
    
  658.         Scheduler.log('Committed: ' + log);
    
  659.         if (log === 'B') {
    
  660.           // Right after B commits, schedule additional updates.
    
  661.           ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
    
  662.             pushToLog('C'),
    
  663.           );
    
  664.           setLog(prevLog => prevLog + 'D');
    
  665.         }
    
  666.       }, [log]);
    
  667. 
    
  668.       return log;
    
  669.     }
    
  670. 
    
  671.     const root = ReactNoop.createRoot();
    
  672.     await act(() => {
    
  673.       root.render(<App />);
    
  674.     });
    
  675.     assertLog(['Committed: ']);
    
  676.     expect(root).toMatchRenderedOutput(null);
    
  677. 
    
  678.     await act(() => {
    
  679.       React.startTransition(() => {
    
  680.         pushToLog('A');
    
  681.       });
    
  682. 
    
  683.       ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
    
  684.         pushToLog('B'),
    
  685.       );
    
  686.     });
    
  687.     if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  688.       assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
    
  689.     } else {
    
  690.       assertLog([
    
  691.         // A and B are pending. B is higher priority, so we'll render that first.
    
  692.         'Committed: B',
    
  693.         // Because A comes first in the queue, we're now in rebase mode. B must
    
  694.         // be rebased on top of A. Also, in a layout effect, we received two new
    
  695.         // updates: C and D. C is user-blocking and D is synchronous.
    
  696.         //
    
  697.         // First render the synchronous update. What we're testing here is that
    
  698.         // B *is not dropped* even though it has lower than sync priority. That's
    
  699.         // because we already committed it. However, this render should not
    
  700.         // include C, because that update wasn't already committed.
    
  701.         'Committed: BD',
    
  702.         'Committed: BCD',
    
  703.         'Committed: ABCD',
    
  704.       ]);
    
  705.     }
    
  706.     expect(root).toMatchRenderedOutput('ABCD');
    
  707.   });
    
  708. 
    
  709.   it('when rebasing, does not exclude updates that were already committed, regardless of priority (classes)', async () => {
    
  710.     let pushToLog;
    
  711.     class App extends React.Component {
    
  712.       state = {log: ''};
    
  713.       pushToLog = msg => {
    
  714.         this.setState(prevState => ({log: prevState.log + msg}));
    
  715.       };
    
  716.       componentDidUpdate() {
    
  717.         Scheduler.log('Committed: ' + this.state.log);
    
  718.         if (this.state.log === 'B') {
    
  719.           // Right after B commits, schedule additional updates.
    
  720.           ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
    
  721.             this.pushToLog('C'),
    
  722.           );
    
  723.           this.pushToLog('D');
    
  724.         }
    
  725.       }
    
  726.       render() {
    
  727.         pushToLog = this.pushToLog;
    
  728.         return this.state.log;
    
  729.       }
    
  730.     }
    
  731. 
    
  732.     const root = ReactNoop.createRoot();
    
  733.     await act(() => {
    
  734.       root.render(<App />);
    
  735.     });
    
  736.     assertLog([]);
    
  737.     expect(root).toMatchRenderedOutput(null);
    
  738. 
    
  739.     await act(() => {
    
  740.       React.startTransition(() => {
    
  741.         pushToLog('A');
    
  742.       });
    
  743.       ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
    
  744.         pushToLog('B'),
    
  745.       );
    
  746.     });
    
  747.     if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  748.       assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
    
  749.     } else {
    
  750.       assertLog([
    
  751.         // A and B are pending. B is higher priority, so we'll render that first.
    
  752.         'Committed: B',
    
  753.         // Because A comes first in the queue, we're now in rebase mode. B must
    
  754.         // be rebased on top of A. Also, in a layout effect, we received two new
    
  755.         // updates: C and D. C is user-blocking and D is synchronous.
    
  756.         //
    
  757.         // First render the synchronous update. What we're testing here is that
    
  758.         // B *is not dropped* even though it has lower than sync priority. That's
    
  759.         // because we already committed it. However, this render should not
    
  760.         // include C, because that update wasn't already committed.
    
  761.         'Committed: BD',
    
  762.         'Committed: BCD',
    
  763.         'Committed: ABCD',
    
  764.       ]);
    
  765.     }
    
  766.     expect(root).toMatchRenderedOutput('ABCD');
    
  767.   });
    
  768. 
    
  769.   it("base state of update queue is initialized to its fiber's memoized state", async () => {
    
  770.     // This test is very weird because it tests an implementation detail but
    
  771.     // is tested in terms of public APIs. When it was originally written, the
    
  772.     // test failed because the update queue was initialized to the state of
    
  773.     // the alternate fiber.
    
  774.     let app;
    
  775.     class App extends React.Component {
    
  776.       state = {prevProp: 'A', count: 0};
    
  777.       static getDerivedStateFromProps(props, state) {
    
  778.         // Add 100 whenever the label prop changes. The prev label is stored
    
  779.         // in state. If the state is dropped incorrectly, we'll fail to detect
    
  780.         // prop changes.
    
  781.         if (props.prop !== state.prevProp) {
    
  782.           return {
    
  783.             prevProp: props.prop,
    
  784.             count: state.count + 100,
    
  785.           };
    
  786.         }
    
  787.         return null;
    
  788.       }
    
  789.       render() {
    
  790.         app = this;
    
  791.         return this.state.count;
    
  792.       }
    
  793.     }
    
  794. 
    
  795.     const root = ReactNoop.createRoot();
    
  796.     await act(() => {
    
  797.       root.render(<App prop="A" />);
    
  798.     });
    
  799.     expect(root).toMatchRenderedOutput('0');
    
  800. 
    
  801.     // Changing the prop causes the count to increase by 100
    
  802.     await act(() => {
    
  803.       root.render(<App prop="B" />);
    
  804.     });
    
  805.     expect(root).toMatchRenderedOutput('100');
    
  806. 
    
  807.     // Now increment the count by 1 with a state update. And, in the same
    
  808.     // batch, change the prop back to its original value.
    
  809.     await act(() => {
    
  810.       root.render(<App prop="A" />);
    
  811.       app.setState(state => ({count: state.count + 1}));
    
  812.     });
    
  813.     // There were two total prop changes, plus an increment.
    
  814.     expect(root).toMatchRenderedOutput('201');
    
  815.   });
    
  816. });