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. /* eslint-disable no-func-assign */
    
  12. 
    
  13. 'use strict';
    
  14. 
    
  15. let React;
    
  16. let textCache;
    
  17. let readText;
    
  18. let resolveText;
    
  19. let ReactNoop;
    
  20. let Scheduler;
    
  21. let Suspense;
    
  22. let useState;
    
  23. let useReducer;
    
  24. let useEffect;
    
  25. let useInsertionEffect;
    
  26. let useLayoutEffect;
    
  27. let useCallback;
    
  28. let useMemo;
    
  29. let useRef;
    
  30. let useImperativeHandle;
    
  31. let useTransition;
    
  32. let useDeferredValue;
    
  33. let forwardRef;
    
  34. let memo;
    
  35. let act;
    
  36. let ContinuousEventPriority;
    
  37. let SuspenseList;
    
  38. let waitForAll;
    
  39. let waitFor;
    
  40. let waitForThrow;
    
  41. let waitForPaint;
    
  42. let assertLog;
    
  43. 
    
  44. describe('ReactHooksWithNoopRenderer', () => {
    
  45.   beforeEach(() => {
    
  46.     jest.resetModules();
    
  47.     jest.useFakeTimers();
    
  48. 
    
  49.     React = require('react');
    
  50.     ReactNoop = require('react-noop-renderer');
    
  51.     Scheduler = require('scheduler');
    
  52.     act = require('internal-test-utils').act;
    
  53.     useState = React.useState;
    
  54.     useReducer = React.useReducer;
    
  55.     useEffect = React.useEffect;
    
  56.     useInsertionEffect = React.useInsertionEffect;
    
  57.     useLayoutEffect = React.useLayoutEffect;
    
  58.     useCallback = React.useCallback;
    
  59.     useMemo = React.useMemo;
    
  60.     useRef = React.useRef;
    
  61.     useImperativeHandle = React.useImperativeHandle;
    
  62.     forwardRef = React.forwardRef;
    
  63.     memo = React.memo;
    
  64.     useTransition = React.useTransition;
    
  65.     useDeferredValue = React.useDeferredValue;
    
  66.     Suspense = React.Suspense;
    
  67.     ContinuousEventPriority =
    
  68.       require('react-reconciler/constants').ContinuousEventPriority;
    
  69.     if (gate(flags => flags.enableSuspenseList)) {
    
  70.       SuspenseList = React.unstable_SuspenseList;
    
  71.     }
    
  72. 
    
  73.     const InternalTestUtils = require('internal-test-utils');
    
  74.     waitForAll = InternalTestUtils.waitForAll;
    
  75.     waitFor = InternalTestUtils.waitFor;
    
  76.     waitForThrow = InternalTestUtils.waitForThrow;
    
  77.     waitForPaint = InternalTestUtils.waitForPaint;
    
  78.     assertLog = InternalTestUtils.assertLog;
    
  79. 
    
  80.     textCache = new Map();
    
  81. 
    
  82.     readText = text => {
    
  83.       const record = textCache.get(text);
    
  84.       if (record !== undefined) {
    
  85.         switch (record.status) {
    
  86.           case 'pending':
    
  87.             throw record.promise;
    
  88.           case 'rejected':
    
  89.             throw Error('Failed to load: ' + text);
    
  90.           case 'resolved':
    
  91.             return text;
    
  92.         }
    
  93.       } else {
    
  94.         let ping;
    
  95.         const promise = new Promise(resolve => (ping = resolve));
    
  96.         const newRecord = {
    
  97.           status: 'pending',
    
  98.           ping: ping,
    
  99.           promise,
    
  100.         };
    
  101.         textCache.set(text, newRecord);
    
  102.         throw promise;
    
  103.       }
    
  104.     };
    
  105. 
    
  106.     resolveText = text => {
    
  107.       const record = textCache.get(text);
    
  108.       if (record !== undefined) {
    
  109.         if (record.status === 'pending') {
    
  110.           Scheduler.log(`Promise resolved [${text}]`);
    
  111.           record.ping();
    
  112.           record.ping = null;
    
  113.           record.status = 'resolved';
    
  114.           clearTimeout(record.promise._timer);
    
  115.           record.promise = null;
    
  116.         }
    
  117.       } else {
    
  118.         const newRecord = {
    
  119.           ping: null,
    
  120.           status: 'resolved',
    
  121.           promise: null,
    
  122.         };
    
  123.         textCache.set(text, newRecord);
    
  124.       }
    
  125.     };
    
  126.   });
    
  127. 
    
  128.   function Text(props) {
    
  129.     Scheduler.log(props.text);
    
  130.     return <span prop={props.text} />;
    
  131.   }
    
  132. 
    
  133.   function AsyncText(props) {
    
  134.     const text = props.text;
    
  135.     try {
    
  136.       readText(text);
    
  137.       Scheduler.log(text);
    
  138.       return <span prop={text} />;
    
  139.     } catch (promise) {
    
  140.       if (typeof promise.then === 'function') {
    
  141.         Scheduler.log(`Suspend! [${text}]`);
    
  142.         if (typeof props.ms === 'number' && promise._timer === undefined) {
    
  143.           promise._timer = setTimeout(() => {
    
  144.             resolveText(text);
    
  145.           }, props.ms);
    
  146.         }
    
  147.       } else {
    
  148.         Scheduler.log(`Error! [${text}]`);
    
  149.       }
    
  150.       throw promise;
    
  151.     }
    
  152.   }
    
  153. 
    
  154.   function advanceTimers(ms) {
    
  155.     // Note: This advances Jest's virtual time but not React's. Use
    
  156.     // ReactNoop.expire for that.
    
  157.     if (typeof ms !== 'number') {
    
  158.       throw new Error('Must specify ms');
    
  159.     }
    
  160.     jest.advanceTimersByTime(ms);
    
  161.     // Wait until the end of the current tick
    
  162.     // We cannot use a timer since we're faking them
    
  163.     return Promise.resolve().then(() => {});
    
  164.   }
    
  165. 
    
  166.   it('resumes after an interruption', async () => {
    
  167.     function Counter(props, ref) {
    
  168.       const [count, updateCount] = useState(0);
    
  169.       useImperativeHandle(ref, () => ({updateCount}));
    
  170.       return <Text text={props.label + ': ' + count} />;
    
  171.     }
    
  172.     Counter = forwardRef(Counter);
    
  173. 
    
  174.     // Initial mount
    
  175.     const counter = React.createRef(null);
    
  176.     ReactNoop.render(<Counter label="Count" ref={counter} />);
    
  177.     await waitForAll(['Count: 0']);
    
  178.     expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  179. 
    
  180.     // Schedule some updates
    
  181.     await act(async () => {
    
  182.       React.startTransition(() => {
    
  183.         counter.current.updateCount(1);
    
  184.         counter.current.updateCount(count => count + 10);
    
  185.       });
    
  186. 
    
  187.       // Partially flush without committing
    
  188.       await waitFor(['Count: 11']);
    
  189.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  190. 
    
  191.       // Interrupt with a high priority update
    
  192.       ReactNoop.flushSync(() => {
    
  193.         ReactNoop.render(<Counter label="Total" />);
    
  194.       });
    
  195.       assertLog(['Total: 0']);
    
  196. 
    
  197.       // Resume rendering
    
  198.       await waitForAll(['Total: 11']);
    
  199.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Total: 11" />);
    
  200.     });
    
  201.   });
    
  202. 
    
  203.   it('throws inside class components', async () => {
    
  204.     class BadCounter extends React.Component {
    
  205.       render() {
    
  206.         const [count] = useState(0);
    
  207.         return <Text text={this.props.label + ': ' + count} />;
    
  208.       }
    
  209.     }
    
  210.     ReactNoop.render(<BadCounter />);
    
  211. 
    
  212.     await waitForThrow(
    
  213.       'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    
  214.         ' one of the following reasons:\n' +
    
  215.         '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    
  216.         '2. You might be breaking the Rules of Hooks\n' +
    
  217.         '3. You might have more than one copy of React in the same app\n' +
    
  218.         'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    
  219.     );
    
  220. 
    
  221.     // Confirm that a subsequent hook works properly.
    
  222.     function GoodCounter(props, ref) {
    
  223.       const [count] = useState(props.initialCount);
    
  224.       return <Text text={count} />;
    
  225.     }
    
  226.     ReactNoop.render(<GoodCounter initialCount={10} />);
    
  227.     await waitForAll([10]);
    
  228.   });
    
  229. 
    
  230.   // @gate !disableModulePatternComponents
    
  231.   it('throws inside module-style components', async () => {
    
  232.     function Counter() {
    
  233.       return {
    
  234.         render() {
    
  235.           const [count] = useState(0);
    
  236.           return <Text text={this.props.label + ': ' + count} />;
    
  237.         },
    
  238.       };
    
  239.     }
    
  240.     ReactNoop.render(<Counter />);
    
  241.     await expect(
    
  242.       async () =>
    
  243.         await waitForThrow(
    
  244.           'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
    
  245.             'for one of the following reasons:\n' +
    
  246.             '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    
  247.             '2. You might be breaking the Rules of Hooks\n' +
    
  248.             '3. You might have more than one copy of React in the same app\n' +
    
  249.             'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    
  250.         ),
    
  251.     ).toErrorDev(
    
  252.       'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
    
  253.         'Change Counter to a class that extends React.Component instead. ' +
    
  254.         "If you can't use a class try assigning the prototype on the function as a workaround. " +
    
  255.         '`Counter.prototype = React.Component.prototype`. ' +
    
  256.         "Don't use an arrow function since it cannot be called with `new` by React.",
    
  257.     );
    
  258. 
    
  259.     // Confirm that a subsequent hook works properly.
    
  260.     function GoodCounter(props) {
    
  261.       const [count] = useState(props.initialCount);
    
  262.       return <Text text={count} />;
    
  263.     }
    
  264.     ReactNoop.render(<GoodCounter initialCount={10} />);
    
  265.     await waitForAll([10]);
    
  266.   });
    
  267. 
    
  268.   it('throws when called outside the render phase', async () => {
    
  269.     expect(() => {
    
  270.       expect(() => useState(0)).toThrow(
    
  271.         "Cannot read property 'useState' of null",
    
  272.       );
    
  273.     }).toErrorDev(
    
  274.       'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    
  275.         ' one of the following reasons:\n' +
    
  276.         '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    
  277.         '2. You might be breaking the Rules of Hooks\n' +
    
  278.         '3. You might have more than one copy of React in the same app\n' +
    
  279.         'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    
  280.       {withoutStack: true},
    
  281.     );
    
  282.   });
    
  283. 
    
  284.   describe('useState', () => {
    
  285.     it('simple mount and update', async () => {
    
  286.       function Counter(props, ref) {
    
  287.         const [count, updateCount] = useState(0);
    
  288.         useImperativeHandle(ref, () => ({updateCount}));
    
  289.         return <Text text={'Count: ' + count} />;
    
  290.       }
    
  291.       Counter = forwardRef(Counter);
    
  292.       const counter = React.createRef(null);
    
  293.       ReactNoop.render(<Counter ref={counter} />);
    
  294.       await waitForAll(['Count: 0']);
    
  295.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  296. 
    
  297.       await act(() => counter.current.updateCount(1));
    
  298.       assertLog(['Count: 1']);
    
  299.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  300. 
    
  301.       await act(() => counter.current.updateCount(count => count + 10));
    
  302.       assertLog(['Count: 11']);
    
  303.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
    
  304.     });
    
  305. 
    
  306.     it('lazy state initializer', async () => {
    
  307.       function Counter(props, ref) {
    
  308.         const [count, updateCount] = useState(() => {
    
  309.           Scheduler.log('getInitialState');
    
  310.           return props.initialState;
    
  311.         });
    
  312.         useImperativeHandle(ref, () => ({updateCount}));
    
  313.         return <Text text={'Count: ' + count} />;
    
  314.       }
    
  315.       Counter = forwardRef(Counter);
    
  316.       const counter = React.createRef(null);
    
  317.       ReactNoop.render(<Counter initialState={42} ref={counter} />);
    
  318.       await waitForAll(['getInitialState', 'Count: 42']);
    
  319.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 42" />);
    
  320. 
    
  321.       await act(() => counter.current.updateCount(7));
    
  322.       assertLog(['Count: 7']);
    
  323.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 7" />);
    
  324.     });
    
  325. 
    
  326.     it('multiple states', async () => {
    
  327.       function Counter(props, ref) {
    
  328.         const [count, updateCount] = useState(0);
    
  329.         const [label, updateLabel] = useState('Count');
    
  330.         useImperativeHandle(ref, () => ({updateCount, updateLabel}));
    
  331.         return <Text text={label + ': ' + count} />;
    
  332.       }
    
  333.       Counter = forwardRef(Counter);
    
  334.       const counter = React.createRef(null);
    
  335.       ReactNoop.render(<Counter ref={counter} />);
    
  336.       await waitForAll(['Count: 0']);
    
  337.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  338. 
    
  339.       await act(() => counter.current.updateCount(7));
    
  340.       assertLog(['Count: 7']);
    
  341. 
    
  342.       await act(() => counter.current.updateLabel('Total'));
    
  343.       assertLog(['Total: 7']);
    
  344.     });
    
  345. 
    
  346.     it('returns the same updater function every time', async () => {
    
  347.       let updater = null;
    
  348.       function Counter() {
    
  349.         const [count, updateCount] = useState(0);
    
  350.         updater = updateCount;
    
  351.         return <Text text={'Count: ' + count} />;
    
  352.       }
    
  353.       ReactNoop.render(<Counter />);
    
  354.       await waitForAll(['Count: 0']);
    
  355.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  356. 
    
  357.       const firstUpdater = updater;
    
  358. 
    
  359.       await act(() => firstUpdater(1));
    
  360.       assertLog(['Count: 1']);
    
  361.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  362. 
    
  363.       const secondUpdater = updater;
    
  364. 
    
  365.       await act(() => firstUpdater(count => count + 10));
    
  366.       assertLog(['Count: 11']);
    
  367.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
    
  368. 
    
  369.       expect(firstUpdater).toBe(secondUpdater);
    
  370.     });
    
  371. 
    
  372.     it('does not warn on set after unmount', async () => {
    
  373.       let _updateCount;
    
  374.       function Counter(props, ref) {
    
  375.         const [, updateCount] = useState(0);
    
  376.         _updateCount = updateCount;
    
  377.         return null;
    
  378.       }
    
  379. 
    
  380.       ReactNoop.render(<Counter />);
    
  381.       await waitForAll([]);
    
  382.       ReactNoop.render(null);
    
  383.       await waitForAll([]);
    
  384.       await act(() => _updateCount(1));
    
  385.     });
    
  386. 
    
  387.     it('works with memo', async () => {
    
  388.       let _updateCount;
    
  389.       function Counter(props) {
    
  390.         const [count, updateCount] = useState(0);
    
  391.         _updateCount = updateCount;
    
  392.         return <Text text={'Count: ' + count} />;
    
  393.       }
    
  394.       Counter = memo(Counter);
    
  395. 
    
  396.       ReactNoop.render(<Counter />);
    
  397.       await waitForAll(['Count: 0']);
    
  398.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  399. 
    
  400.       ReactNoop.render(<Counter />);
    
  401.       await waitForAll([]);
    
  402.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  403. 
    
  404.       await act(() => _updateCount(1));
    
  405.       assertLog(['Count: 1']);
    
  406.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  407.     });
    
  408.   });
    
  409. 
    
  410.   describe('updates during the render phase', () => {
    
  411.     it('restarts the render function and applies the new updates on top', async () => {
    
  412.       function ScrollView({row: newRow}) {
    
  413.         const [isScrollingDown, setIsScrollingDown] = useState(false);
    
  414.         const [row, setRow] = useState(null);
    
  415. 
    
  416.         if (row !== newRow) {
    
  417.           // Row changed since last render. Update isScrollingDown.
    
  418.           setIsScrollingDown(row !== null && newRow > row);
    
  419.           setRow(newRow);
    
  420.         }
    
  421. 
    
  422.         return <Text text={`Scrolling down: ${isScrollingDown}`} />;
    
  423.       }
    
  424. 
    
  425.       ReactNoop.render(<ScrollView row={1} />);
    
  426.       await waitForAll(['Scrolling down: false']);
    
  427.       expect(ReactNoop).toMatchRenderedOutput(
    
  428.         <span prop="Scrolling down: false" />,
    
  429.       );
    
  430. 
    
  431.       ReactNoop.render(<ScrollView row={5} />);
    
  432.       await waitForAll(['Scrolling down: true']);
    
  433.       expect(ReactNoop).toMatchRenderedOutput(
    
  434.         <span prop="Scrolling down: true" />,
    
  435.       );
    
  436. 
    
  437.       ReactNoop.render(<ScrollView row={5} />);
    
  438.       await waitForAll(['Scrolling down: true']);
    
  439.       expect(ReactNoop).toMatchRenderedOutput(
    
  440.         <span prop="Scrolling down: true" />,
    
  441.       );
    
  442. 
    
  443.       ReactNoop.render(<ScrollView row={10} />);
    
  444.       await waitForAll(['Scrolling down: true']);
    
  445.       expect(ReactNoop).toMatchRenderedOutput(
    
  446.         <span prop="Scrolling down: true" />,
    
  447.       );
    
  448. 
    
  449.       ReactNoop.render(<ScrollView row={2} />);
    
  450.       await waitForAll(['Scrolling down: false']);
    
  451.       expect(ReactNoop).toMatchRenderedOutput(
    
  452.         <span prop="Scrolling down: false" />,
    
  453.       );
    
  454. 
    
  455.       ReactNoop.render(<ScrollView row={2} />);
    
  456.       await waitForAll(['Scrolling down: false']);
    
  457.       expect(ReactNoop).toMatchRenderedOutput(
    
  458.         <span prop="Scrolling down: false" />,
    
  459.       );
    
  460.     });
    
  461. 
    
  462.     it('warns about render phase update on a different component', async () => {
    
  463.       let setStep;
    
  464.       function Foo() {
    
  465.         const [step, _setStep] = useState(0);
    
  466.         setStep = _setStep;
    
  467.         return <Text text={`Foo [${step}]`} />;
    
  468.       }
    
  469. 
    
  470.       function Bar({triggerUpdate}) {
    
  471.         if (triggerUpdate) {
    
  472.           setStep(x => x + 1);
    
  473.         }
    
  474.         return <Text text="Bar" />;
    
  475.       }
    
  476. 
    
  477.       const root = ReactNoop.createRoot();
    
  478. 
    
  479.       await act(() => {
    
  480.         root.render(
    
  481.           <>
    
  482.             <Foo />
    
  483.             <Bar />
    
  484.           </>,
    
  485.         );
    
  486.       });
    
  487.       assertLog(['Foo [0]', 'Bar']);
    
  488. 
    
  489.       // Bar will update Foo during its render phase. React should warn.
    
  490.       root.render(
    
  491.         <>
    
  492.           <Foo />
    
  493.           <Bar triggerUpdate={true} />
    
  494.         </>,
    
  495.       );
    
  496.       await expect(
    
  497.         async () => await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']),
    
  498.       ).toErrorDev([
    
  499.         'Cannot update a component (`Foo`) while rendering a ' +
    
  500.           'different component (`Bar`). To locate the bad setState() call inside `Bar`',
    
  501.       ]);
    
  502. 
    
  503.       // It should not warn again (deduplication).
    
  504.       await act(async () => {
    
  505.         root.render(
    
  506.           <>
    
  507.             <Foo />
    
  508.             <Bar triggerUpdate={true} />
    
  509.           </>,
    
  510.         );
    
  511.         await waitForAll(['Foo [1]', 'Bar', 'Foo [2]']);
    
  512.       });
    
  513.     });
    
  514. 
    
  515.     it('keeps restarting until there are no more new updates', async () => {
    
  516.       function Counter({row: newRow}) {
    
  517.         const [count, setCount] = useState(0);
    
  518.         if (count < 3) {
    
  519.           setCount(count + 1);
    
  520.         }
    
  521.         Scheduler.log('Render: ' + count);
    
  522.         return <Text text={count} />;
    
  523.       }
    
  524. 
    
  525.       ReactNoop.render(<Counter />);
    
  526.       await waitForAll(['Render: 0', 'Render: 1', 'Render: 2', 'Render: 3', 3]);
    
  527.       expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
    
  528.     });
    
  529. 
    
  530.     it('updates multiple times within same render function', async () => {
    
  531.       function Counter({row: newRow}) {
    
  532.         const [count, setCount] = useState(0);
    
  533.         if (count < 12) {
    
  534.           setCount(c => c + 1);
    
  535.           setCount(c => c + 1);
    
  536.           setCount(c => c + 1);
    
  537.         }
    
  538.         Scheduler.log('Render: ' + count);
    
  539.         return <Text text={count} />;
    
  540.       }
    
  541. 
    
  542.       ReactNoop.render(<Counter />);
    
  543.       await waitForAll([
    
  544.         // Should increase by three each time
    
  545.         'Render: 0',
    
  546.         'Render: 3',
    
  547.         'Render: 6',
    
  548.         'Render: 9',
    
  549.         'Render: 12',
    
  550.         12,
    
  551.       ]);
    
  552.       expect(ReactNoop).toMatchRenderedOutput(<span prop={12} />);
    
  553.     });
    
  554. 
    
  555.     it('throws after too many iterations', async () => {
    
  556.       function Counter({row: newRow}) {
    
  557.         const [count, setCount] = useState(0);
    
  558.         setCount(count + 1);
    
  559.         Scheduler.log('Render: ' + count);
    
  560.         return <Text text={count} />;
    
  561.       }
    
  562.       ReactNoop.render(<Counter />);
    
  563.       await waitForThrow(
    
  564.         'Too many re-renders. React limits the number of renders to prevent ' +
    
  565.           'an infinite loop.',
    
  566.       );
    
  567.     });
    
  568. 
    
  569.     it('works with useReducer', async () => {
    
  570.       function reducer(state, action) {
    
  571.         return action === 'increment' ? state + 1 : state;
    
  572.       }
    
  573.       function Counter({row: newRow}) {
    
  574.         const [count, dispatch] = useReducer(reducer, 0);
    
  575.         if (count < 3) {
    
  576.           dispatch('increment');
    
  577.         }
    
  578.         Scheduler.log('Render: ' + count);
    
  579.         return <Text text={count} />;
    
  580.       }
    
  581. 
    
  582.       ReactNoop.render(<Counter />);
    
  583.       await waitForAll(['Render: 0', 'Render: 1', 'Render: 2', 'Render: 3', 3]);
    
  584.       expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
    
  585.     });
    
  586. 
    
  587.     it('uses reducer passed at time of render, not time of dispatch', async () => {
    
  588.       // This test is a bit contrived but it demonstrates a subtle edge case.
    
  589. 
    
  590.       // Reducer A increments by 1. Reducer B increments by 10.
    
  591.       function reducerA(state, action) {
    
  592.         switch (action) {
    
  593.           case 'increment':
    
  594.             return state + 1;
    
  595.           case 'reset':
    
  596.             return 0;
    
  597.         }
    
  598.       }
    
  599.       function reducerB(state, action) {
    
  600.         switch (action) {
    
  601.           case 'increment':
    
  602.             return state + 10;
    
  603.           case 'reset':
    
  604.             return 0;
    
  605.         }
    
  606.       }
    
  607. 
    
  608.       function Counter({row: newRow}, ref) {
    
  609.         const [reducer, setReducer] = useState(() => reducerA);
    
  610.         const [count, dispatch] = useReducer(reducer, 0);
    
  611.         useImperativeHandle(ref, () => ({dispatch}));
    
  612.         if (count < 20) {
    
  613.           dispatch('increment');
    
  614.           // Swap reducers each time we increment
    
  615.           if (reducer === reducerA) {
    
  616.             setReducer(() => reducerB);
    
  617.           } else {
    
  618.             setReducer(() => reducerA);
    
  619.           }
    
  620.         }
    
  621.         Scheduler.log('Render: ' + count);
    
  622.         return <Text text={count} />;
    
  623.       }
    
  624.       Counter = forwardRef(Counter);
    
  625.       const counter = React.createRef(null);
    
  626.       ReactNoop.render(<Counter ref={counter} />);
    
  627.       await waitForAll([
    
  628.         // The count should increase by alternating amounts of 10 and 1
    
  629.         // until we reach 21.
    
  630.         'Render: 0',
    
  631.         'Render: 10',
    
  632.         'Render: 11',
    
  633.         'Render: 21',
    
  634.         21,
    
  635.       ]);
    
  636.       expect(ReactNoop).toMatchRenderedOutput(<span prop={21} />);
    
  637. 
    
  638.       // Test that it works on update, too. This time the log is a bit different
    
  639.       // because we started with reducerB instead of reducerA.
    
  640.       await act(() => {
    
  641.         counter.current.dispatch('reset');
    
  642.       });
    
  643.       ReactNoop.render(<Counter ref={counter} />);
    
  644.       assertLog([
    
  645.         'Render: 0',
    
  646.         'Render: 1',
    
  647.         'Render: 11',
    
  648.         'Render: 12',
    
  649.         'Render: 22',
    
  650.         22,
    
  651.       ]);
    
  652.       expect(ReactNoop).toMatchRenderedOutput(<span prop={22} />);
    
  653.     });
    
  654. 
    
  655.     it('discards render phase updates if something suspends', async () => {
    
  656.       const thenable = {then() {}};
    
  657.       function Foo({signal}) {
    
  658.         return (
    
  659.           <Suspense fallback="Loading...">
    
  660.             <Bar signal={signal} />
    
  661.           </Suspense>
    
  662.         );
    
  663.       }
    
  664. 
    
  665.       function Bar({signal: newSignal}) {
    
  666.         const [counter, setCounter] = useState(0);
    
  667.         const [signal, setSignal] = useState(true);
    
  668. 
    
  669.         // Increment a counter every time the signal changes
    
  670.         if (signal !== newSignal) {
    
  671.           setCounter(c => c + 1);
    
  672.           setSignal(newSignal);
    
  673.           if (counter === 0) {
    
  674.             // We're suspending during a render that includes render phase
    
  675.             // updates. Those updates should not persist to the next render.
    
  676.             Scheduler.log('Suspend!');
    
  677.             throw thenable;
    
  678.           }
    
  679.         }
    
  680. 
    
  681.         return <Text text={counter} />;
    
  682.       }
    
  683. 
    
  684.       const root = ReactNoop.createRoot();
    
  685.       root.render(<Foo signal={true} />);
    
  686. 
    
  687.       await waitForAll([0]);
    
  688.       expect(root).toMatchRenderedOutput(<span prop={0} />);
    
  689. 
    
  690.       React.startTransition(() => {
    
  691.         root.render(<Foo signal={false} />);
    
  692.       });
    
  693.       await waitForAll(['Suspend!']);
    
  694.       expect(root).toMatchRenderedOutput(<span prop={0} />);
    
  695. 
    
  696.       // Rendering again should suspend again.
    
  697.       React.startTransition(() => {
    
  698.         root.render(<Foo signal={false} />);
    
  699.       });
    
  700.       await waitForAll(['Suspend!']);
    
  701.     });
    
  702. 
    
  703.     it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
    
  704.       const thenable = {then() {}};
    
  705.       function Foo({signal}) {
    
  706.         return (
    
  707.           <Suspense fallback="Loading...">
    
  708.             <Bar signal={signal} />
    
  709.           </Suspense>
    
  710.         );
    
  711.       }
    
  712. 
    
  713.       let setLabel;
    
  714.       function Bar({signal: newSignal}) {
    
  715.         const [counter, setCounter] = useState(0);
    
  716. 
    
  717.         if (counter === 1) {
    
  718.           // We're suspending during a render that includes render phase
    
  719.           // updates. Those updates should not persist to the next render.
    
  720.           Scheduler.log('Suspend!');
    
  721.           throw thenable;
    
  722.         }
    
  723. 
    
  724.         const [signal, setSignal] = useState(true);
    
  725. 
    
  726.         // Increment a counter every time the signal changes
    
  727.         if (signal !== newSignal) {
    
  728.           setCounter(c => c + 1);
    
  729.           setSignal(newSignal);
    
  730.         }
    
  731. 
    
  732.         const [label, _setLabel] = useState('A');
    
  733.         setLabel = _setLabel;
    
  734. 
    
  735.         return <Text text={`${label}:${counter}`} />;
    
  736.       }
    
  737. 
    
  738.       const root = ReactNoop.createRoot();
    
  739.       root.render(<Foo signal={true} />);
    
  740. 
    
  741.       await waitForAll(['A:0']);
    
  742.       expect(root).toMatchRenderedOutput(<span prop="A:0" />);
    
  743. 
    
  744.       await act(async () => {
    
  745.         React.startTransition(() => {
    
  746.           root.render(<Foo signal={false} />);
    
  747.           setLabel('B');
    
  748.         });
    
  749. 
    
  750.         await waitForAll(['Suspend!']);
    
  751.         expect(root).toMatchRenderedOutput(<span prop="A:0" />);
    
  752. 
    
  753.         // Rendering again should suspend again.
    
  754.         React.startTransition(() => {
    
  755.           root.render(<Foo signal={false} />);
    
  756.         });
    
  757.         await waitForAll(['Suspend!']);
    
  758. 
    
  759.         // Flip the signal back to "cancel" the update. However, the update to
    
  760.         // label should still proceed. It shouldn't have been dropped.
    
  761.         React.startTransition(() => {
    
  762.           root.render(<Foo signal={true} />);
    
  763.         });
    
  764.         await waitForAll(['B:0']);
    
  765.         expect(root).toMatchRenderedOutput(<span prop="B:0" />);
    
  766.       });
    
  767.     });
    
  768. 
    
  769.     it('regression: render phase updates cause lower pri work to be dropped', async () => {
    
  770.       let setRow;
    
  771.       function ScrollView() {
    
  772.         const [row, _setRow] = useState(10);
    
  773.         setRow = _setRow;
    
  774. 
    
  775.         const [scrollDirection, setScrollDirection] = useState('Up');
    
  776.         const [prevRow, setPrevRow] = useState(null);
    
  777. 
    
  778.         if (prevRow !== row) {
    
  779.           setScrollDirection(prevRow !== null && row > prevRow ? 'Down' : 'Up');
    
  780.           setPrevRow(row);
    
  781.         }
    
  782. 
    
  783.         return <Text text={scrollDirection} />;
    
  784.       }
    
  785. 
    
  786.       const root = ReactNoop.createRoot();
    
  787. 
    
  788.       await act(() => {
    
  789.         root.render(<ScrollView row={10} />);
    
  790.       });
    
  791.       assertLog(['Up']);
    
  792.       expect(root).toMatchRenderedOutput(<span prop="Up" />);
    
  793. 
    
  794.       await act(() => {
    
  795.         ReactNoop.discreteUpdates(() => {
    
  796.           setRow(5);
    
  797.         });
    
  798.         React.startTransition(() => {
    
  799.           setRow(20);
    
  800.         });
    
  801.       });
    
  802.       assertLog(['Up', 'Down']);
    
  803.       expect(root).toMatchRenderedOutput(<span prop="Down" />);
    
  804.     });
    
  805. 
    
  806.     // TODO: This should probably warn
    
  807.     it('calling startTransition inside render phase', async () => {
    
  808.       function App() {
    
  809.         const [counter, setCounter] = useState(0);
    
  810. 
    
  811.         if (counter === 0) {
    
  812.           React.startTransition(() => {
    
  813.             setCounter(c => c + 1);
    
  814.           });
    
  815.         }
    
  816. 
    
  817.         return <Text text={counter} />;
    
  818.       }
    
  819. 
    
  820.       const root = ReactNoop.createRoot();
    
  821.       root.render(<App />);
    
  822.       await waitForAll([1]);
    
  823.       expect(root).toMatchRenderedOutput(<span prop={1} />);
    
  824.     });
    
  825.   });
    
  826. 
    
  827.   describe('useReducer', () => {
    
  828.     it('simple mount and update', async () => {
    
  829.       const INCREMENT = 'INCREMENT';
    
  830.       const DECREMENT = 'DECREMENT';
    
  831. 
    
  832.       function reducer(state, action) {
    
  833.         switch (action) {
    
  834.           case 'INCREMENT':
    
  835.             return state + 1;
    
  836.           case 'DECREMENT':
    
  837.             return state - 1;
    
  838.           default:
    
  839.             return state;
    
  840.         }
    
  841.       }
    
  842. 
    
  843.       function Counter(props, ref) {
    
  844.         const [count, dispatch] = useReducer(reducer, 0);
    
  845.         useImperativeHandle(ref, () => ({dispatch}));
    
  846.         return <Text text={'Count: ' + count} />;
    
  847.       }
    
  848.       Counter = forwardRef(Counter);
    
  849.       const counter = React.createRef(null);
    
  850.       ReactNoop.render(<Counter ref={counter} />);
    
  851.       await waitForAll(['Count: 0']);
    
  852.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  853. 
    
  854.       await act(() => counter.current.dispatch(INCREMENT));
    
  855.       assertLog(['Count: 1']);
    
  856.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  857.       await act(() => {
    
  858.         counter.current.dispatch(DECREMENT);
    
  859.         counter.current.dispatch(DECREMENT);
    
  860.         counter.current.dispatch(DECREMENT);
    
  861.       });
    
  862. 
    
  863.       assertLog(['Count: -2']);
    
  864.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: -2" />);
    
  865.     });
    
  866. 
    
  867.     it('lazy init', async () => {
    
  868.       const INCREMENT = 'INCREMENT';
    
  869.       const DECREMENT = 'DECREMENT';
    
  870. 
    
  871.       function reducer(state, action) {
    
  872.         switch (action) {
    
  873.           case 'INCREMENT':
    
  874.             return state + 1;
    
  875.           case 'DECREMENT':
    
  876.             return state - 1;
    
  877.           default:
    
  878.             return state;
    
  879.         }
    
  880.       }
    
  881. 
    
  882.       function Counter(props, ref) {
    
  883.         const [count, dispatch] = useReducer(reducer, props, p => {
    
  884.           Scheduler.log('Init');
    
  885.           return p.initialCount;
    
  886.         });
    
  887.         useImperativeHandle(ref, () => ({dispatch}));
    
  888.         return <Text text={'Count: ' + count} />;
    
  889.       }
    
  890.       Counter = forwardRef(Counter);
    
  891.       const counter = React.createRef(null);
    
  892.       ReactNoop.render(<Counter initialCount={10} ref={counter} />);
    
  893.       await waitForAll(['Init', 'Count: 10']);
    
  894.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 10" />);
    
  895. 
    
  896.       await act(() => counter.current.dispatch(INCREMENT));
    
  897.       assertLog(['Count: 11']);
    
  898.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
    
  899. 
    
  900.       await act(() => {
    
  901.         counter.current.dispatch(DECREMENT);
    
  902.         counter.current.dispatch(DECREMENT);
    
  903.         counter.current.dispatch(DECREMENT);
    
  904.       });
    
  905. 
    
  906.       assertLog(['Count: 8']);
    
  907.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 8" />);
    
  908.     });
    
  909. 
    
  910.     // Regression test for https://github.com/facebook/react/issues/14360
    
  911.     it('handles dispatches with mixed priorities', async () => {
    
  912.       const INCREMENT = 'INCREMENT';
    
  913. 
    
  914.       function reducer(state, action) {
    
  915.         return action === INCREMENT ? state + 1 : state;
    
  916.       }
    
  917. 
    
  918.       function Counter(props, ref) {
    
  919.         const [count, dispatch] = useReducer(reducer, 0);
    
  920.         useImperativeHandle(ref, () => ({dispatch}));
    
  921.         return <Text text={'Count: ' + count} />;
    
  922.       }
    
  923. 
    
  924.       Counter = forwardRef(Counter);
    
  925.       const counter = React.createRef(null);
    
  926.       ReactNoop.render(<Counter ref={counter} />);
    
  927. 
    
  928.       await waitForAll(['Count: 0']);
    
  929.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  930. 
    
  931.       ReactNoop.batchedUpdates(() => {
    
  932.         counter.current.dispatch(INCREMENT);
    
  933.         counter.current.dispatch(INCREMENT);
    
  934.         counter.current.dispatch(INCREMENT);
    
  935.       });
    
  936. 
    
  937.       ReactNoop.flushSync(() => {
    
  938.         counter.current.dispatch(INCREMENT);
    
  939.       });
    
  940.       if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  941.         assertLog(['Count: 4']);
    
  942.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 4" />);
    
  943.       } else {
    
  944.         assertLog(['Count: 1']);
    
  945.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  946.         await waitForAll(['Count: 4']);
    
  947.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 4" />);
    
  948.       }
    
  949.     });
    
  950.   });
    
  951. 
    
  952.   describe('useEffect', () => {
    
  953.     it('simple mount and update', async () => {
    
  954.       function Counter(props) {
    
  955.         useEffect(() => {
    
  956.           Scheduler.log(`Passive effect [${props.count}]`);
    
  957.         });
    
  958.         return <Text text={'Count: ' + props.count} />;
    
  959.       }
    
  960.       await act(async () => {
    
  961.         ReactNoop.render(<Counter count={0} />, () =>
    
  962.           Scheduler.log('Sync effect'),
    
  963.         );
    
  964.         await waitFor(['Count: 0', 'Sync effect']);
    
  965.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  966.         // Effects are deferred until after the commit
    
  967.         await waitForAll(['Passive effect [0]']);
    
  968.       });
    
  969. 
    
  970.       await act(async () => {
    
  971.         ReactNoop.render(<Counter count={1} />, () =>
    
  972.           Scheduler.log('Sync effect'),
    
  973.         );
    
  974.         await waitFor(['Count: 1', 'Sync effect']);
    
  975.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  976.         // Effects are deferred until after the commit
    
  977.         await waitForAll(['Passive effect [1]']);
    
  978.       });
    
  979.     });
    
  980. 
    
  981.     it('flushes passive effects even with sibling deletions', async () => {
    
  982.       function LayoutEffect(props) {
    
  983.         useLayoutEffect(() => {
    
  984.           Scheduler.log(`Layout effect`);
    
  985.         });
    
  986.         return <Text text="Layout" />;
    
  987.       }
    
  988.       function PassiveEffect(props) {
    
  989.         useEffect(() => {
    
  990.           Scheduler.log(`Passive effect`);
    
  991.         }, []);
    
  992.         return <Text text="Passive" />;
    
  993.       }
    
  994.       const passive = <PassiveEffect key="p" />;
    
  995.       await act(async () => {
    
  996.         ReactNoop.render([<LayoutEffect key="l" />, passive]);
    
  997.         await waitFor(['Layout', 'Passive', 'Layout effect']);
    
  998.         expect(ReactNoop).toMatchRenderedOutput(
    
  999.           <>
    
  1000.             <span prop="Layout" />
    
  1001.             <span prop="Passive" />
    
  1002.           </>,
    
  1003.         );
    
  1004.         // Destroying the first child shouldn't prevent the passive effect from
    
  1005.         // being executed
    
  1006.         ReactNoop.render([passive]);
    
  1007.         await waitForAll(['Passive effect']);
    
  1008.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Passive" />);
    
  1009.       });
    
  1010.       // exiting act calls flushPassiveEffects(), but there are none left to flush.
    
  1011.       assertLog([]);
    
  1012.     });
    
  1013. 
    
  1014.     it('flushes passive effects even if siblings schedule an update', async () => {
    
  1015.       function PassiveEffect(props) {
    
  1016.         useEffect(() => {
    
  1017.           Scheduler.log('Passive effect');
    
  1018.         });
    
  1019.         return <Text text="Passive" />;
    
  1020.       }
    
  1021.       function LayoutEffect(props) {
    
  1022.         const [count, setCount] = useState(0);
    
  1023.         useLayoutEffect(() => {
    
  1024.           // Scheduling work shouldn't interfere with the queued passive effect
    
  1025.           if (count === 0) {
    
  1026.             setCount(1);
    
  1027.           }
    
  1028.           Scheduler.log('Layout effect ' + count);
    
  1029.         });
    
  1030.         return <Text text="Layout" />;
    
  1031.       }
    
  1032. 
    
  1033.       ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
    
  1034. 
    
  1035.       await act(async () => {
    
  1036.         await waitForAll([
    
  1037.           'Passive',
    
  1038.           'Layout',
    
  1039.           'Layout effect 0',
    
  1040.           'Passive effect',
    
  1041.           'Layout',
    
  1042.           'Layout effect 1',
    
  1043.         ]);
    
  1044.       });
    
  1045. 
    
  1046.       expect(ReactNoop).toMatchRenderedOutput(
    
  1047.         <>
    
  1048.           <span prop="Passive" />
    
  1049.           <span prop="Layout" />
    
  1050.         </>,
    
  1051.       );
    
  1052.     });
    
  1053. 
    
  1054.     it('flushes passive effects even if siblings schedule a new root', async () => {
    
  1055.       function PassiveEffect(props) {
    
  1056.         useEffect(() => {
    
  1057.           Scheduler.log('Passive effect');
    
  1058.         }, []);
    
  1059.         return <Text text="Passive" />;
    
  1060.       }
    
  1061.       function LayoutEffect(props) {
    
  1062.         useLayoutEffect(() => {
    
  1063.           Scheduler.log('Layout effect');
    
  1064.           // Scheduling work shouldn't interfere with the queued passive effect
    
  1065.           ReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');
    
  1066.         });
    
  1067.         return <Text text="Layout" />;
    
  1068.       }
    
  1069.       await act(async () => {
    
  1070.         ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
    
  1071.         await waitForAll([
    
  1072.           'Passive',
    
  1073.           'Layout',
    
  1074.           'Layout effect',
    
  1075.           'Passive effect',
    
  1076.           'New Root',
    
  1077.         ]);
    
  1078.         expect(ReactNoop).toMatchRenderedOutput(
    
  1079.           <>
    
  1080.             <span prop="Passive" />
    
  1081.             <span prop="Layout" />
    
  1082.           </>,
    
  1083.         );
    
  1084.       });
    
  1085.     });
    
  1086. 
    
  1087.     it(
    
  1088.       'flushes effects serially by flushing old effects before flushing ' +
    
  1089.         "new ones, if they haven't already fired",
    
  1090.       async () => {
    
  1091.         function getCommittedText() {
    
  1092.           const children = ReactNoop.getChildrenAsJSX();
    
  1093.           if (children === null) {
    
  1094.             return null;
    
  1095.           }
    
  1096.           return children.props.prop;
    
  1097.         }
    
  1098. 
    
  1099.         function Counter(props) {
    
  1100.           useEffect(() => {
    
  1101.             Scheduler.log(
    
  1102.               `Committed state when effect was fired: ${getCommittedText()}`,
    
  1103.             );
    
  1104.           });
    
  1105.           return <Text text={props.count} />;
    
  1106.         }
    
  1107.         await act(async () => {
    
  1108.           ReactNoop.render(<Counter count={0} />, () =>
    
  1109.             Scheduler.log('Sync effect'),
    
  1110.           );
    
  1111.           await waitFor([0, 'Sync effect']);
    
  1112.           expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
    
  1113.           // Before the effects have a chance to flush, schedule another update
    
  1114.           ReactNoop.render(<Counter count={1} />, () =>
    
  1115.             Scheduler.log('Sync effect'),
    
  1116.           );
    
  1117.           await waitFor([
    
  1118.             // The previous effect flushes before the reconciliation
    
  1119.             'Committed state when effect was fired: 0',
    
  1120.             1,
    
  1121.             'Sync effect',
    
  1122.           ]);
    
  1123.           expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
    
  1124.         });
    
  1125. 
    
  1126.         assertLog(['Committed state when effect was fired: 1']);
    
  1127.       },
    
  1128.     );
    
  1129. 
    
  1130.     it('defers passive effect destroy functions during unmount', async () => {
    
  1131.       function Child({bar, foo}) {
    
  1132.         React.useEffect(() => {
    
  1133.           Scheduler.log('passive bar create');
    
  1134.           return () => {
    
  1135.             Scheduler.log('passive bar destroy');
    
  1136.           };
    
  1137.         }, [bar]);
    
  1138.         React.useLayoutEffect(() => {
    
  1139.           Scheduler.log('layout bar create');
    
  1140.           return () => {
    
  1141.             Scheduler.log('layout bar destroy');
    
  1142.           };
    
  1143.         }, [bar]);
    
  1144.         React.useEffect(() => {
    
  1145.           Scheduler.log('passive foo create');
    
  1146.           return () => {
    
  1147.             Scheduler.log('passive foo destroy');
    
  1148.           };
    
  1149.         }, [foo]);
    
  1150.         React.useLayoutEffect(() => {
    
  1151.           Scheduler.log('layout foo create');
    
  1152.           return () => {
    
  1153.             Scheduler.log('layout foo destroy');
    
  1154.           };
    
  1155.         }, [foo]);
    
  1156.         Scheduler.log('render');
    
  1157.         return null;
    
  1158.       }
    
  1159. 
    
  1160.       await act(async () => {
    
  1161.         ReactNoop.render(<Child bar={1} foo={1} />, () =>
    
  1162.           Scheduler.log('Sync effect'),
    
  1163.         );
    
  1164.         await waitFor([
    
  1165.           'render',
    
  1166.           'layout bar create',
    
  1167.           'layout foo create',
    
  1168.           'Sync effect',
    
  1169.         ]);
    
  1170.         // Effects are deferred until after the commit
    
  1171.         await waitForAll(['passive bar create', 'passive foo create']);
    
  1172.       });
    
  1173. 
    
  1174.       // This update exists to test an internal implementation detail:
    
  1175.       // Effects without updating dependencies lose their layout/passive tag during an update.
    
  1176.       await act(async () => {
    
  1177.         ReactNoop.render(<Child bar={1} foo={2} />, () =>
    
  1178.           Scheduler.log('Sync effect'),
    
  1179.         );
    
  1180.         await waitFor([
    
  1181.           'render',
    
  1182.           'layout foo destroy',
    
  1183.           'layout foo create',
    
  1184.           'Sync effect',
    
  1185.         ]);
    
  1186.         // Effects are deferred until after the commit
    
  1187.         await waitForAll(['passive foo destroy', 'passive foo create']);
    
  1188.       });
    
  1189. 
    
  1190.       // Unmount the component and verify that passive destroy functions are deferred until post-commit.
    
  1191.       await act(async () => {
    
  1192.         ReactNoop.render(null, () => Scheduler.log('Sync effect'));
    
  1193.         await waitFor([
    
  1194.           'layout bar destroy',
    
  1195.           'layout foo destroy',
    
  1196.           'Sync effect',
    
  1197.         ]);
    
  1198.         // Effects are deferred until after the commit
    
  1199.         await waitForAll(['passive bar destroy', 'passive foo destroy']);
    
  1200.       });
    
  1201.     });
    
  1202. 
    
  1203.     it('does not warn about state updates for unmounted components with pending passive unmounts', async () => {
    
  1204.       let completePendingRequest = null;
    
  1205.       function Component() {
    
  1206.         Scheduler.log('Component');
    
  1207.         const [didLoad, setDidLoad] = React.useState(false);
    
  1208.         React.useLayoutEffect(() => {
    
  1209.           Scheduler.log('layout create');
    
  1210.           return () => {
    
  1211.             Scheduler.log('layout destroy');
    
  1212.           };
    
  1213.         }, []);
    
  1214.         React.useEffect(() => {
    
  1215.           Scheduler.log('passive create');
    
  1216.           // Mimic an XHR request with a complete handler that updates state.
    
  1217.           completePendingRequest = () => setDidLoad(true);
    
  1218.           return () => {
    
  1219.             Scheduler.log('passive destroy');
    
  1220.           };
    
  1221.         }, []);
    
  1222.         return didLoad;
    
  1223.       }
    
  1224. 
    
  1225.       await act(async () => {
    
  1226.         ReactNoop.renderToRootWithID(<Component />, 'root', () =>
    
  1227.           Scheduler.log('Sync effect'),
    
  1228.         );
    
  1229.         await waitFor(['Component', 'layout create', 'Sync effect']);
    
  1230.         ReactNoop.flushPassiveEffects();
    
  1231.         assertLog(['passive create']);
    
  1232. 
    
  1233.         // Unmount but don't process pending passive destroy function
    
  1234.         ReactNoop.unmountRootWithID('root');
    
  1235.         await waitFor(['layout destroy']);
    
  1236. 
    
  1237.         // Simulate an XHR completing, which will cause a state update-
    
  1238.         // but should not log a warning.
    
  1239.         completePendingRequest();
    
  1240. 
    
  1241.         ReactNoop.flushPassiveEffects();
    
  1242.         assertLog(['passive destroy']);
    
  1243.       });
    
  1244.     });
    
  1245. 
    
  1246.     it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', async () => {
    
  1247.       let setParentState = null;
    
  1248.       const setChildStates = [];
    
  1249. 
    
  1250.       function Parent() {
    
  1251.         const [state, setState] = useState(true);
    
  1252.         setParentState = setState;
    
  1253.         Scheduler.log(`Parent ${state} render`);
    
  1254.         useLayoutEffect(() => {
    
  1255.           Scheduler.log(`Parent ${state} commit`);
    
  1256.         });
    
  1257.         if (state) {
    
  1258.           return (
    
  1259.             <>
    
  1260.               <Child label="one" />
    
  1261.               <Child label="two" />
    
  1262.             </>
    
  1263.           );
    
  1264.         } else {
    
  1265.           return null;
    
  1266.         }
    
  1267.       }
    
  1268. 
    
  1269.       function Child({label}) {
    
  1270.         const [state, setState] = useState(0);
    
  1271.         useLayoutEffect(() => {
    
  1272.           Scheduler.log(`Child ${label} commit`);
    
  1273.         });
    
  1274.         useEffect(() => {
    
  1275.           setChildStates.push(setState);
    
  1276.           Scheduler.log(`Child ${label} passive create`);
    
  1277.           return () => {
    
  1278.             Scheduler.log(`Child ${label} passive destroy`);
    
  1279.           };
    
  1280.         }, []);
    
  1281.         Scheduler.log(`Child ${label} render`);
    
  1282.         return state;
    
  1283.       }
    
  1284. 
    
  1285.       // Schedule debounced state update for child (prob a no-op for this test)
    
  1286.       // later tick: schedule unmount for parent
    
  1287.       // start process unmount (but don't flush passive effectS)
    
  1288.       // State update on child
    
  1289.       await act(async () => {
    
  1290.         ReactNoop.render(<Parent />);
    
  1291.         await waitFor([
    
  1292.           'Parent true render',
    
  1293.           'Child one render',
    
  1294.           'Child two render',
    
  1295.           'Child one commit',
    
  1296.           'Child two commit',
    
  1297.           'Parent true commit',
    
  1298.           'Child one passive create',
    
  1299.           'Child two passive create',
    
  1300.         ]);
    
  1301. 
    
  1302.         // Update children.
    
  1303.         setChildStates.forEach(setChildState => setChildState(1));
    
  1304.         await waitFor([
    
  1305.           'Child one render',
    
  1306.           'Child two render',
    
  1307.           'Child one commit',
    
  1308.           'Child two commit',
    
  1309.         ]);
    
  1310. 
    
  1311.         // Schedule another update for children, and partially process it.
    
  1312.         React.startTransition(() => {
    
  1313.           setChildStates.forEach(setChildState => setChildState(2));
    
  1314.         });
    
  1315.         await waitFor(['Child one render']);
    
  1316. 
    
  1317.         // Schedule unmount for the parent that unmounts children with pending update.
    
  1318.         ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
    
  1319.           setParentState(false);
    
  1320.         });
    
  1321.         await waitForPaint(['Parent false render', 'Parent false commit']);
    
  1322. 
    
  1323.         // Schedule updates for children too (which should be ignored)
    
  1324.         setChildStates.forEach(setChildState => setChildState(2));
    
  1325.         await waitForAll([
    
  1326.           'Child one passive destroy',
    
  1327.           'Child two passive destroy',
    
  1328.         ]);
    
  1329.       });
    
  1330.     });
    
  1331. 
    
  1332.     it('does not warn about state updates for unmounted components with no pending passive unmounts', async () => {
    
  1333.       let completePendingRequest = null;
    
  1334.       function Component() {
    
  1335.         Scheduler.log('Component');
    
  1336.         const [didLoad, setDidLoad] = React.useState(false);
    
  1337.         React.useLayoutEffect(() => {
    
  1338.           Scheduler.log('layout create');
    
  1339.           // Mimic an XHR request with a complete handler that updates state.
    
  1340.           completePendingRequest = () => setDidLoad(true);
    
  1341.           return () => {
    
  1342.             Scheduler.log('layout destroy');
    
  1343.           };
    
  1344.         }, []);
    
  1345.         return didLoad;
    
  1346.       }
    
  1347. 
    
  1348.       await act(async () => {
    
  1349.         ReactNoop.renderToRootWithID(<Component />, 'root', () =>
    
  1350.           Scheduler.log('Sync effect'),
    
  1351.         );
    
  1352.         await waitFor(['Component', 'layout create', 'Sync effect']);
    
  1353. 
    
  1354.         // Unmount but don't process pending passive destroy function
    
  1355.         ReactNoop.unmountRootWithID('root');
    
  1356.         await waitFor(['layout destroy']);
    
  1357. 
    
  1358.         // Simulate an XHR completing.
    
  1359.         completePendingRequest();
    
  1360.       });
    
  1361.     });
    
  1362. 
    
  1363.     it('does not warn if there are pending passive unmount effects but not for the current fiber', async () => {
    
  1364.       let completePendingRequest = null;
    
  1365.       function ComponentWithXHR() {
    
  1366.         Scheduler.log('Component');
    
  1367.         const [didLoad, setDidLoad] = React.useState(false);
    
  1368.         React.useLayoutEffect(() => {
    
  1369.           Scheduler.log('a:layout create');
    
  1370.           return () => {
    
  1371.             Scheduler.log('a:layout destroy');
    
  1372.           };
    
  1373.         }, []);
    
  1374.         React.useEffect(() => {
    
  1375.           Scheduler.log('a:passive create');
    
  1376.           // Mimic an XHR request with a complete handler that updates state.
    
  1377.           completePendingRequest = () => setDidLoad(true);
    
  1378.         }, []);
    
  1379.         return didLoad;
    
  1380.       }
    
  1381. 
    
  1382.       function ComponentWithPendingPassiveUnmount() {
    
  1383.         React.useEffect(() => {
    
  1384.           Scheduler.log('b:passive create');
    
  1385.           return () => {
    
  1386.             Scheduler.log('b:passive destroy');
    
  1387.           };
    
  1388.         }, []);
    
  1389.         return null;
    
  1390.       }
    
  1391. 
    
  1392.       await act(async () => {
    
  1393.         ReactNoop.renderToRootWithID(
    
  1394.           <>
    
  1395.             <ComponentWithXHR />
    
  1396.             <ComponentWithPendingPassiveUnmount />
    
  1397.           </>,
    
  1398.           'root',
    
  1399.           () => Scheduler.log('Sync effect'),
    
  1400.         );
    
  1401.         await waitFor(['Component', 'a:layout create', 'Sync effect']);
    
  1402.         ReactNoop.flushPassiveEffects();
    
  1403.         assertLog(['a:passive create', 'b:passive create']);
    
  1404. 
    
  1405.         // Unmount but don't process pending passive destroy function
    
  1406.         ReactNoop.unmountRootWithID('root');
    
  1407.         await waitFor(['a:layout destroy']);
    
  1408. 
    
  1409.         // Simulate an XHR completing in the component without a pending passive effect..
    
  1410.         completePendingRequest();
    
  1411.       });
    
  1412.     });
    
  1413. 
    
  1414.     it('does not warn if there are updates after pending passive unmount effects have been flushed', async () => {
    
  1415.       let updaterFunction;
    
  1416. 
    
  1417.       function Component() {
    
  1418.         Scheduler.log('Component');
    
  1419.         const [state, setState] = React.useState(false);
    
  1420.         updaterFunction = setState;
    
  1421.         React.useEffect(() => {
    
  1422.           Scheduler.log('passive create');
    
  1423.           return () => {
    
  1424.             Scheduler.log('passive destroy');
    
  1425.           };
    
  1426.         }, []);
    
  1427.         return state;
    
  1428.       }
    
  1429. 
    
  1430.       await act(() => {
    
  1431.         ReactNoop.renderToRootWithID(<Component />, 'root', () =>
    
  1432.           Scheduler.log('Sync effect'),
    
  1433.         );
    
  1434.       });
    
  1435.       assertLog(['Component', 'Sync effect', 'passive create']);
    
  1436. 
    
  1437.       ReactNoop.unmountRootWithID('root');
    
  1438.       await waitForAll(['passive destroy']);
    
  1439. 
    
  1440.       await act(() => {
    
  1441.         updaterFunction(true);
    
  1442.       });
    
  1443.     });
    
  1444. 
    
  1445.     it('does not show a warning when a component updates its own state from within passive unmount function', async () => {
    
  1446.       function Component() {
    
  1447.         Scheduler.log('Component');
    
  1448.         const [didLoad, setDidLoad] = React.useState(false);
    
  1449.         React.useEffect(() => {
    
  1450.           Scheduler.log('passive create');
    
  1451.           return () => {
    
  1452.             setDidLoad(true);
    
  1453.             Scheduler.log('passive destroy');
    
  1454.           };
    
  1455.         }, []);
    
  1456.         return didLoad;
    
  1457.       }
    
  1458. 
    
  1459.       await act(async () => {
    
  1460.         ReactNoop.renderToRootWithID(<Component />, 'root', () =>
    
  1461.           Scheduler.log('Sync effect'),
    
  1462.         );
    
  1463.         await waitFor(['Component', 'Sync effect', 'passive create']);
    
  1464. 
    
  1465.         // Unmount but don't process pending passive destroy function
    
  1466.         ReactNoop.unmountRootWithID('root');
    
  1467.         await waitForAll(['passive destroy']);
    
  1468.       });
    
  1469.     });
    
  1470. 
    
  1471.     it('does not show a warning when a component updates a child state from within passive unmount function', async () => {
    
  1472.       function Parent() {
    
  1473.         Scheduler.log('Parent');
    
  1474.         const updaterRef = useRef(null);
    
  1475.         React.useEffect(() => {
    
  1476.           Scheduler.log('Parent passive create');
    
  1477.           return () => {
    
  1478.             updaterRef.current(true);
    
  1479.             Scheduler.log('Parent passive destroy');
    
  1480.           };
    
  1481.         }, []);
    
  1482.         return <Child updaterRef={updaterRef} />;
    
  1483.       }
    
  1484. 
    
  1485.       function Child({updaterRef}) {
    
  1486.         Scheduler.log('Child');
    
  1487.         const [state, setState] = React.useState(false);
    
  1488.         React.useEffect(() => {
    
  1489.           Scheduler.log('Child passive create');
    
  1490.           updaterRef.current = setState;
    
  1491.         }, []);
    
  1492.         return state;
    
  1493.       }
    
  1494. 
    
  1495.       await act(async () => {
    
  1496.         ReactNoop.renderToRootWithID(<Parent />, 'root');
    
  1497.         await waitFor([
    
  1498.           'Parent',
    
  1499.           'Child',
    
  1500.           'Child passive create',
    
  1501.           'Parent passive create',
    
  1502.         ]);
    
  1503. 
    
  1504.         // Unmount but don't process pending passive destroy function
    
  1505.         ReactNoop.unmountRootWithID('root');
    
  1506.         await waitForAll(['Parent passive destroy']);
    
  1507.       });
    
  1508.     });
    
  1509. 
    
  1510.     it('does not show a warning when a component updates a parents state from within passive unmount function', async () => {
    
  1511.       function Parent() {
    
  1512.         const [state, setState] = React.useState(false);
    
  1513.         Scheduler.log('Parent');
    
  1514.         return <Child setState={setState} state={state} />;
    
  1515.       }
    
  1516. 
    
  1517.       function Child({setState, state}) {
    
  1518.         Scheduler.log('Child');
    
  1519.         React.useEffect(() => {
    
  1520.           Scheduler.log('Child passive create');
    
  1521.           return () => {
    
  1522.             Scheduler.log('Child passive destroy');
    
  1523.             setState(true);
    
  1524.           };
    
  1525.         }, []);
    
  1526.         return state;
    
  1527.       }
    
  1528. 
    
  1529.       await act(async () => {
    
  1530.         ReactNoop.renderToRootWithID(<Parent />, 'root');
    
  1531.         await waitFor(['Parent', 'Child', 'Child passive create']);
    
  1532. 
    
  1533.         // Unmount but don't process pending passive destroy function
    
  1534.         ReactNoop.unmountRootWithID('root');
    
  1535.         await waitForAll(['Child passive destroy']);
    
  1536.       });
    
  1537.     });
    
  1538. 
    
  1539.     it('updates have async priority', async () => {
    
  1540.       function Counter(props) {
    
  1541.         const [count, updateCount] = useState('(empty)');
    
  1542.         useEffect(() => {
    
  1543.           Scheduler.log(`Schedule update [${props.count}]`);
    
  1544.           updateCount(props.count);
    
  1545.         }, [props.count]);
    
  1546.         return <Text text={'Count: ' + count} />;
    
  1547.       }
    
  1548.       await act(async () => {
    
  1549.         ReactNoop.render(<Counter count={0} />, () =>
    
  1550.           Scheduler.log('Sync effect'),
    
  1551.         );
    
  1552.         await waitFor(['Count: (empty)', 'Sync effect']);
    
  1553.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
    
  1554.         ReactNoop.flushPassiveEffects();
    
  1555.         assertLog(['Schedule update [0]']);
    
  1556.         await waitForAll(['Count: 0']);
    
  1557.       });
    
  1558. 
    
  1559.       await act(async () => {
    
  1560.         ReactNoop.render(<Counter count={1} />, () =>
    
  1561.           Scheduler.log('Sync effect'),
    
  1562.         );
    
  1563.         await waitFor(['Count: 0', 'Sync effect']);
    
  1564.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1565.         ReactNoop.flushPassiveEffects();
    
  1566.         assertLog(['Schedule update [1]']);
    
  1567.         await waitForAll(['Count: 1']);
    
  1568.       });
    
  1569.     });
    
  1570. 
    
  1571.     it('updates have async priority even if effects are flushed early', async () => {
    
  1572.       function Counter(props) {
    
  1573.         const [count, updateCount] = useState('(empty)');
    
  1574.         useEffect(() => {
    
  1575.           Scheduler.log(`Schedule update [${props.count}]`);
    
  1576.           updateCount(props.count);
    
  1577.         }, [props.count]);
    
  1578.         return <Text text={'Count: ' + count} />;
    
  1579.       }
    
  1580.       await act(async () => {
    
  1581.         ReactNoop.render(<Counter count={0} />, () =>
    
  1582.           Scheduler.log('Sync effect'),
    
  1583.         );
    
  1584.         await waitFor(['Count: (empty)', 'Sync effect']);
    
  1585.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
    
  1586. 
    
  1587.         // Rendering again should flush the previous commit's effects
    
  1588.         if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  1589.           ReactNoop.render(<Counter count={1} />, () =>
    
  1590.             Scheduler.log('Sync effect'),
    
  1591.           );
    
  1592.         } else {
    
  1593.           React.startTransition(() => {
    
  1594.             ReactNoop.render(<Counter count={1} />, () =>
    
  1595.               Scheduler.log('Sync effect'),
    
  1596.             );
    
  1597.           });
    
  1598.         }
    
  1599. 
    
  1600.         await waitFor(['Schedule update [0]', 'Count: 0']);
    
  1601. 
    
  1602.         if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  1603.           expect(ReactNoop).toMatchRenderedOutput(
    
  1604.             <span prop="Count: (empty)" />,
    
  1605.           );
    
  1606.           await waitFor(['Sync effect']);
    
  1607.           expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1608. 
    
  1609.           ReactNoop.flushPassiveEffects();
    
  1610.           assertLog(['Schedule update [1]']);
    
  1611.           await waitForAll(['Count: 1']);
    
  1612.         } else {
    
  1613.           expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1614.           await waitFor([
    
  1615.             'Count: 0',
    
  1616.             'Sync effect',
    
  1617.             'Schedule update [1]',
    
  1618.             'Count: 1',
    
  1619.           ]);
    
  1620.         }
    
  1621. 
    
  1622.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1623.       });
    
  1624.     });
    
  1625. 
    
  1626.     it('does not flush non-discrete passive effects when flushing sync', async () => {
    
  1627.       let _updateCount;
    
  1628.       function Counter(props) {
    
  1629.         const [count, updateCount] = useState(0);
    
  1630.         _updateCount = updateCount;
    
  1631.         useEffect(() => {
    
  1632.           Scheduler.log(`Will set count to 1`);
    
  1633.           updateCount(1);
    
  1634.         }, []);
    
  1635.         return <Text text={'Count: ' + count} />;
    
  1636.       }
    
  1637. 
    
  1638.       ReactNoop.render(<Counter count={0} />, () =>
    
  1639.         Scheduler.log('Sync effect'),
    
  1640.       );
    
  1641.       await waitFor(['Count: 0', 'Sync effect']);
    
  1642.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1643.       // A flush sync doesn't cause the passive effects to fire.
    
  1644.       // So we haven't added the other update yet.
    
  1645.       await act(() => {
    
  1646.         ReactNoop.flushSync(() => {
    
  1647.           _updateCount(2);
    
  1648.         });
    
  1649.       });
    
  1650. 
    
  1651.       // As a result we, somewhat surprisingly, commit them in the opposite order.
    
  1652.       // This should be fine because any non-discrete set of work doesn't guarantee order
    
  1653.       // and easily could've happened slightly later too.
    
  1654.       if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  1655.         assertLog(['Will set count to 1', 'Count: 1']);
    
  1656.       } else {
    
  1657.         assertLog(['Will set count to 1', 'Count: 2', 'Count: 1']);
    
  1658.       }
    
  1659. 
    
  1660.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1661.     });
    
  1662. 
    
  1663.     it(
    
  1664.       'in legacy mode, useEffect is deferred and updates finish synchronously ' +
    
  1665.         '(in a single batch)',
    
  1666.       async () => {
    
  1667.         function Counter(props) {
    
  1668.           const [count, updateCount] = useState('(empty)');
    
  1669.           useEffect(() => {
    
  1670.             // Update multiple times. These should all be batched together in
    
  1671.             // a single render.
    
  1672.             updateCount(props.count);
    
  1673.             updateCount(props.count);
    
  1674.             updateCount(props.count);
    
  1675.             updateCount(props.count);
    
  1676.             updateCount(props.count);
    
  1677.             updateCount(props.count);
    
  1678.           }, [props.count]);
    
  1679.           return <Text text={'Count: ' + count} />;
    
  1680.         }
    
  1681.         await act(() => {
    
  1682.           ReactNoop.flushSync(() => {
    
  1683.             ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
    
  1684.           });
    
  1685. 
    
  1686.           // Even in legacy mode, effects are deferred until after paint
    
  1687.           assertLog(['Count: (empty)']);
    
  1688.           expect(ReactNoop).toMatchRenderedOutput(
    
  1689.             <span prop="Count: (empty)" />,
    
  1690.           );
    
  1691.         });
    
  1692. 
    
  1693.         // effects get forced on exiting act()
    
  1694.         // There were multiple updates, but there should only be a
    
  1695.         // single render
    
  1696.         assertLog(['Count: 0']);
    
  1697.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1698.       },
    
  1699.     );
    
  1700. 
    
  1701.     it('flushSync is not allowed', async () => {
    
  1702.       function Counter(props) {
    
  1703.         const [count, updateCount] = useState('(empty)');
    
  1704.         useEffect(() => {
    
  1705.           Scheduler.log(`Schedule update [${props.count}]`);
    
  1706.           ReactNoop.flushSync(() => {
    
  1707.             updateCount(props.count);
    
  1708.           });
    
  1709.           assertLog([`Schedule update [${props.count}]`]);
    
  1710.           // This shouldn't flush synchronously.
    
  1711.           expect(ReactNoop).not.toMatchRenderedOutput(
    
  1712.             <span prop={`Count: ${props.count}`} />,
    
  1713.           );
    
  1714.         }, [props.count]);
    
  1715.         return <Text text={'Count: ' + count} />;
    
  1716.       }
    
  1717.       await expect(async () => {
    
  1718.         await act(async () => {
    
  1719.           ReactNoop.render(<Counter count={0} />, () =>
    
  1720.             Scheduler.log('Sync effect'),
    
  1721.           );
    
  1722.           await waitFor(['Count: (empty)', 'Sync effect']);
    
  1723.           expect(ReactNoop).toMatchRenderedOutput(
    
  1724.             <span prop="Count: (empty)" />,
    
  1725.           );
    
  1726.         });
    
  1727.       }).toErrorDev('flushSync was called from inside a lifecycle method');
    
  1728.       assertLog([`Count: 0`]);
    
  1729.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1730.     });
    
  1731. 
    
  1732.     it('unmounts previous effect', async () => {
    
  1733.       function Counter(props) {
    
  1734.         useEffect(() => {
    
  1735.           Scheduler.log(`Did create [${props.count}]`);
    
  1736.           return () => {
    
  1737.             Scheduler.log(`Did destroy [${props.count}]`);
    
  1738.           };
    
  1739.         });
    
  1740.         return <Text text={'Count: ' + props.count} />;
    
  1741.       }
    
  1742.       await act(async () => {
    
  1743.         ReactNoop.render(<Counter count={0} />, () =>
    
  1744.           Scheduler.log('Sync effect'),
    
  1745.         );
    
  1746.         await waitFor(['Count: 0', 'Sync effect']);
    
  1747.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1748.       });
    
  1749. 
    
  1750.       assertLog(['Did create [0]']);
    
  1751. 
    
  1752.       await act(async () => {
    
  1753.         ReactNoop.render(<Counter count={1} />, () =>
    
  1754.           Scheduler.log('Sync effect'),
    
  1755.         );
    
  1756.         await waitFor(['Count: 1', 'Sync effect']);
    
  1757.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1758.       });
    
  1759. 
    
  1760.       assertLog(['Did destroy [0]', 'Did create [1]']);
    
  1761.     });
    
  1762. 
    
  1763.     it('unmounts on deletion', async () => {
    
  1764.       function Counter(props) {
    
  1765.         useEffect(() => {
    
  1766.           Scheduler.log(`Did create [${props.count}]`);
    
  1767.           return () => {
    
  1768.             Scheduler.log(`Did destroy [${props.count}]`);
    
  1769.           };
    
  1770.         });
    
  1771.         return <Text text={'Count: ' + props.count} />;
    
  1772.       }
    
  1773.       await act(async () => {
    
  1774.         ReactNoop.render(<Counter count={0} />, () =>
    
  1775.           Scheduler.log('Sync effect'),
    
  1776.         );
    
  1777.         await waitFor(['Count: 0', 'Sync effect']);
    
  1778.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1779.       });
    
  1780. 
    
  1781.       assertLog(['Did create [0]']);
    
  1782. 
    
  1783.       ReactNoop.render(null);
    
  1784.       await waitForAll(['Did destroy [0]']);
    
  1785.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  1786.     });
    
  1787. 
    
  1788.     it('unmounts on deletion after skipped effect', async () => {
    
  1789.       function Counter(props) {
    
  1790.         useEffect(() => {
    
  1791.           Scheduler.log(`Did create [${props.count}]`);
    
  1792.           return () => {
    
  1793.             Scheduler.log(`Did destroy [${props.count}]`);
    
  1794.           };
    
  1795.         }, []);
    
  1796.         return <Text text={'Count: ' + props.count} />;
    
  1797.       }
    
  1798.       await act(async () => {
    
  1799.         ReactNoop.render(<Counter count={0} />, () =>
    
  1800.           Scheduler.log('Sync effect'),
    
  1801.         );
    
  1802.         await waitFor(['Count: 0', 'Sync effect']);
    
  1803.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1804.       });
    
  1805. 
    
  1806.       assertLog(['Did create [0]']);
    
  1807. 
    
  1808.       await act(async () => {
    
  1809.         ReactNoop.render(<Counter count={1} />, () =>
    
  1810.           Scheduler.log('Sync effect'),
    
  1811.         );
    
  1812.         await waitFor(['Count: 1', 'Sync effect']);
    
  1813.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1814.       });
    
  1815. 
    
  1816.       assertLog([]);
    
  1817. 
    
  1818.       ReactNoop.render(null);
    
  1819.       await waitForAll(['Did destroy [0]']);
    
  1820.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  1821.     });
    
  1822. 
    
  1823.     it('always fires effects if no dependencies are provided', async () => {
    
  1824.       function effect() {
    
  1825.         Scheduler.log(`Did create`);
    
  1826.         return () => {
    
  1827.           Scheduler.log(`Did destroy`);
    
  1828.         };
    
  1829.       }
    
  1830.       function Counter(props) {
    
  1831.         useEffect(effect);
    
  1832.         return <Text text={'Count: ' + props.count} />;
    
  1833.       }
    
  1834.       await act(async () => {
    
  1835.         ReactNoop.render(<Counter count={0} />, () =>
    
  1836.           Scheduler.log('Sync effect'),
    
  1837.         );
    
  1838.         await waitFor(['Count: 0', 'Sync effect']);
    
  1839.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1840.       });
    
  1841. 
    
  1842.       assertLog(['Did create']);
    
  1843. 
    
  1844.       await act(async () => {
    
  1845.         ReactNoop.render(<Counter count={1} />, () =>
    
  1846.           Scheduler.log('Sync effect'),
    
  1847.         );
    
  1848.         await waitFor(['Count: 1', 'Sync effect']);
    
  1849.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1850.       });
    
  1851. 
    
  1852.       assertLog(['Did destroy', 'Did create']);
    
  1853. 
    
  1854.       ReactNoop.render(null);
    
  1855.       await waitForAll(['Did destroy']);
    
  1856.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  1857.     });
    
  1858. 
    
  1859.     it('skips effect if inputs have not changed', async () => {
    
  1860.       function Counter(props) {
    
  1861.         const text = `${props.label}: ${props.count}`;
    
  1862.         useEffect(() => {
    
  1863.           Scheduler.log(`Did create [${text}]`);
    
  1864.           return () => {
    
  1865.             Scheduler.log(`Did destroy [${text}]`);
    
  1866.           };
    
  1867.         }, [props.label, props.count]);
    
  1868.         return <Text text={text} />;
    
  1869.       }
    
  1870.       await act(async () => {
    
  1871.         ReactNoop.render(<Counter label="Count" count={0} />, () =>
    
  1872.           Scheduler.log('Sync effect'),
    
  1873.         );
    
  1874.         await waitFor(['Count: 0', 'Sync effect']);
    
  1875.       });
    
  1876. 
    
  1877.       assertLog(['Did create [Count: 0]']);
    
  1878.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1879. 
    
  1880.       await act(async () => {
    
  1881.         ReactNoop.render(<Counter label="Count" count={1} />, () =>
    
  1882.           Scheduler.log('Sync effect'),
    
  1883.         );
    
  1884.         // Count changed
    
  1885.         await waitFor(['Count: 1', 'Sync effect']);
    
  1886.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1887.       });
    
  1888. 
    
  1889.       assertLog(['Did destroy [Count: 0]', 'Did create [Count: 1]']);
    
  1890. 
    
  1891.       await act(async () => {
    
  1892.         ReactNoop.render(<Counter label="Count" count={1} />, () =>
    
  1893.           Scheduler.log('Sync effect'),
    
  1894.         );
    
  1895.         // Nothing changed, so no effect should have fired
    
  1896.         await waitFor(['Count: 1', 'Sync effect']);
    
  1897.       });
    
  1898. 
    
  1899.       assertLog([]);
    
  1900.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1901. 
    
  1902.       await act(async () => {
    
  1903.         ReactNoop.render(<Counter label="Total" count={1} />, () =>
    
  1904.           Scheduler.log('Sync effect'),
    
  1905.         );
    
  1906.         // Label changed
    
  1907.         await waitFor(['Total: 1', 'Sync effect']);
    
  1908.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Total: 1" />);
    
  1909.       });
    
  1910. 
    
  1911.       assertLog(['Did destroy [Count: 1]', 'Did create [Total: 1]']);
    
  1912.     });
    
  1913. 
    
  1914.     it('multiple effects', async () => {
    
  1915.       function Counter(props) {
    
  1916.         useEffect(() => {
    
  1917.           Scheduler.log(`Did commit 1 [${props.count}]`);
    
  1918.         });
    
  1919.         useEffect(() => {
    
  1920.           Scheduler.log(`Did commit 2 [${props.count}]`);
    
  1921.         });
    
  1922.         return <Text text={'Count: ' + props.count} />;
    
  1923.       }
    
  1924.       await act(async () => {
    
  1925.         ReactNoop.render(<Counter count={0} />, () =>
    
  1926.           Scheduler.log('Sync effect'),
    
  1927.         );
    
  1928.         await waitFor(['Count: 0', 'Sync effect']);
    
  1929.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1930.       });
    
  1931. 
    
  1932.       assertLog(['Did commit 1 [0]', 'Did commit 2 [0]']);
    
  1933. 
    
  1934.       await act(async () => {
    
  1935.         ReactNoop.render(<Counter count={1} />, () =>
    
  1936.           Scheduler.log('Sync effect'),
    
  1937.         );
    
  1938.         await waitFor(['Count: 1', 'Sync effect']);
    
  1939.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1940.       });
    
  1941.       assertLog(['Did commit 1 [1]', 'Did commit 2 [1]']);
    
  1942.     });
    
  1943. 
    
  1944.     it('unmounts all previous effects before creating any new ones', async () => {
    
  1945.       function Counter(props) {
    
  1946.         useEffect(() => {
    
  1947.           Scheduler.log(`Mount A [${props.count}]`);
    
  1948.           return () => {
    
  1949.             Scheduler.log(`Unmount A [${props.count}]`);
    
  1950.           };
    
  1951.         });
    
  1952.         useEffect(() => {
    
  1953.           Scheduler.log(`Mount B [${props.count}]`);
    
  1954.           return () => {
    
  1955.             Scheduler.log(`Unmount B [${props.count}]`);
    
  1956.           };
    
  1957.         });
    
  1958.         return <Text text={'Count: ' + props.count} />;
    
  1959.       }
    
  1960.       await act(async () => {
    
  1961.         ReactNoop.render(<Counter count={0} />, () =>
    
  1962.           Scheduler.log('Sync effect'),
    
  1963.         );
    
  1964.         await waitFor(['Count: 0', 'Sync effect']);
    
  1965.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  1966.       });
    
  1967. 
    
  1968.       assertLog(['Mount A [0]', 'Mount B [0]']);
    
  1969. 
    
  1970.       await act(async () => {
    
  1971.         ReactNoop.render(<Counter count={1} />, () =>
    
  1972.           Scheduler.log('Sync effect'),
    
  1973.         );
    
  1974.         await waitFor(['Count: 1', 'Sync effect']);
    
  1975.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  1976.       });
    
  1977.       assertLog([
    
  1978.         'Unmount A [0]',
    
  1979.         'Unmount B [0]',
    
  1980.         'Mount A [1]',
    
  1981.         'Mount B [1]',
    
  1982.       ]);
    
  1983.     });
    
  1984. 
    
  1985.     it('unmounts all previous effects between siblings before creating any new ones', async () => {
    
  1986.       function Counter({count, label}) {
    
  1987.         useEffect(() => {
    
  1988.           Scheduler.log(`Mount ${label} [${count}]`);
    
  1989.           return () => {
    
  1990.             Scheduler.log(`Unmount ${label} [${count}]`);
    
  1991.           };
    
  1992.         });
    
  1993.         return <Text text={`${label} ${count}`} />;
    
  1994.       }
    
  1995.       await act(async () => {
    
  1996.         ReactNoop.render(
    
  1997.           <>
    
  1998.             <Counter label="A" count={0} />
    
  1999.             <Counter label="B" count={0} />
    
  2000.           </>,
    
  2001.           () => Scheduler.log('Sync effect'),
    
  2002.         );
    
  2003.         await waitFor(['A 0', 'B 0', 'Sync effect']);
    
  2004.         expect(ReactNoop).toMatchRenderedOutput(
    
  2005.           <>
    
  2006.             <span prop="A 0" />
    
  2007.             <span prop="B 0" />
    
  2008.           </>,
    
  2009.         );
    
  2010.       });
    
  2011. 
    
  2012.       assertLog(['Mount A [0]', 'Mount B [0]']);
    
  2013. 
    
  2014.       await act(async () => {
    
  2015.         ReactNoop.render(
    
  2016.           <>
    
  2017.             <Counter label="A" count={1} />
    
  2018.             <Counter label="B" count={1} />
    
  2019.           </>,
    
  2020.           () => Scheduler.log('Sync effect'),
    
  2021.         );
    
  2022.         await waitFor(['A 1', 'B 1', 'Sync effect']);
    
  2023.         expect(ReactNoop).toMatchRenderedOutput(
    
  2024.           <>
    
  2025.             <span prop="A 1" />
    
  2026.             <span prop="B 1" />
    
  2027.           </>,
    
  2028.         );
    
  2029.       });
    
  2030.       assertLog([
    
  2031.         'Unmount A [0]',
    
  2032.         'Unmount B [0]',
    
  2033.         'Mount A [1]',
    
  2034.         'Mount B [1]',
    
  2035.       ]);
    
  2036. 
    
  2037.       await act(async () => {
    
  2038.         ReactNoop.render(
    
  2039.           <>
    
  2040.             <Counter label="B" count={2} />
    
  2041.             <Counter label="C" count={0} />
    
  2042.           </>,
    
  2043.           () => Scheduler.log('Sync effect'),
    
  2044.         );
    
  2045.         await waitFor(['B 2', 'C 0', 'Sync effect']);
    
  2046.         expect(ReactNoop).toMatchRenderedOutput(
    
  2047.           <>
    
  2048.             <span prop="B 2" />
    
  2049.             <span prop="C 0" />
    
  2050.           </>,
    
  2051.         );
    
  2052.       });
    
  2053.       assertLog([
    
  2054.         'Unmount A [1]',
    
  2055.         'Unmount B [1]',
    
  2056.         'Mount B [2]',
    
  2057.         'Mount C [0]',
    
  2058.       ]);
    
  2059.     });
    
  2060. 
    
  2061.     it('handles errors in create on mount', async () => {
    
  2062.       function Counter(props) {
    
  2063.         useEffect(() => {
    
  2064.           Scheduler.log(`Mount A [${props.count}]`);
    
  2065.           return () => {
    
  2066.             Scheduler.log(`Unmount A [${props.count}]`);
    
  2067.           };
    
  2068.         });
    
  2069.         useEffect(() => {
    
  2070.           Scheduler.log('Oops!');
    
  2071.           throw new Error('Oops!');
    
  2072.           // eslint-disable-next-line no-unreachable
    
  2073.           Scheduler.log(`Mount B [${props.count}]`);
    
  2074.           return () => {
    
  2075.             Scheduler.log(`Unmount B [${props.count}]`);
    
  2076.           };
    
  2077.         });
    
  2078.         return <Text text={'Count: ' + props.count} />;
    
  2079.       }
    
  2080.       await act(async () => {
    
  2081.         ReactNoop.render(<Counter count={0} />, () =>
    
  2082.           Scheduler.log('Sync effect'),
    
  2083.         );
    
  2084.         await waitFor(['Count: 0', 'Sync effect']);
    
  2085.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  2086.         expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
    
  2087.       });
    
  2088. 
    
  2089.       assertLog([
    
  2090.         'Mount A [0]',
    
  2091.         'Oops!',
    
  2092.         // Clean up effect A. There's no effect B to clean-up, because it
    
  2093.         // never mounted.
    
  2094.         'Unmount A [0]',
    
  2095.       ]);
    
  2096.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  2097.     });
    
  2098. 
    
  2099.     it('handles errors in create on update', async () => {
    
  2100.       function Counter(props) {
    
  2101.         useEffect(() => {
    
  2102.           Scheduler.log(`Mount A [${props.count}]`);
    
  2103.           return () => {
    
  2104.             Scheduler.log(`Unmount A [${props.count}]`);
    
  2105.           };
    
  2106.         });
    
  2107.         useEffect(() => {
    
  2108.           if (props.count === 1) {
    
  2109.             Scheduler.log('Oops!');
    
  2110.             throw new Error('Oops!');
    
  2111.           }
    
  2112.           Scheduler.log(`Mount B [${props.count}]`);
    
  2113.           return () => {
    
  2114.             Scheduler.log(`Unmount B [${props.count}]`);
    
  2115.           };
    
  2116.         });
    
  2117.         return <Text text={'Count: ' + props.count} />;
    
  2118.       }
    
  2119.       await act(async () => {
    
  2120.         ReactNoop.render(<Counter count={0} />, () =>
    
  2121.           Scheduler.log('Sync effect'),
    
  2122.         );
    
  2123.         await waitFor(['Count: 0', 'Sync effect']);
    
  2124.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  2125.         ReactNoop.flushPassiveEffects();
    
  2126.         assertLog(['Mount A [0]', 'Mount B [0]']);
    
  2127.       });
    
  2128. 
    
  2129.       await act(async () => {
    
  2130.         // This update will trigger an error
    
  2131.         ReactNoop.render(<Counter count={1} />, () =>
    
  2132.           Scheduler.log('Sync effect'),
    
  2133.         );
    
  2134.         await waitFor(['Count: 1', 'Sync effect']);
    
  2135.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  2136.         expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
    
  2137.         assertLog(['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!']);
    
  2138.         expect(ReactNoop).toMatchRenderedOutput(null);
    
  2139.       });
    
  2140.       assertLog([
    
  2141.         // Clean up effect A runs passively on unmount.
    
  2142.         // There's no effect B to clean-up, because it never mounted.
    
  2143.         'Unmount A [1]',
    
  2144.       ]);
    
  2145.     });
    
  2146. 
    
  2147.     it('handles errors in destroy on update', async () => {
    
  2148.       function Counter(props) {
    
  2149.         useEffect(() => {
    
  2150.           Scheduler.log(`Mount A [${props.count}]`);
    
  2151.           return () => {
    
  2152.             Scheduler.log('Oops!');
    
  2153.             if (props.count === 0) {
    
  2154.               throw new Error('Oops!');
    
  2155.             }
    
  2156.           };
    
  2157.         });
    
  2158.         useEffect(() => {
    
  2159.           Scheduler.log(`Mount B [${props.count}]`);
    
  2160.           return () => {
    
  2161.             Scheduler.log(`Unmount B [${props.count}]`);
    
  2162.           };
    
  2163.         });
    
  2164.         return <Text text={'Count: ' + props.count} />;
    
  2165.       }
    
  2166. 
    
  2167.       await act(async () => {
    
  2168.         ReactNoop.render(<Counter count={0} />, () =>
    
  2169.           Scheduler.log('Sync effect'),
    
  2170.         );
    
  2171.         await waitFor(['Count: 0', 'Sync effect']);
    
  2172.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  2173.         ReactNoop.flushPassiveEffects();
    
  2174.         assertLog(['Mount A [0]', 'Mount B [0]']);
    
  2175.       });
    
  2176. 
    
  2177.       await act(async () => {
    
  2178.         // This update will trigger an error during passive effect unmount
    
  2179.         ReactNoop.render(<Counter count={1} />, () =>
    
  2180.           Scheduler.log('Sync effect'),
    
  2181.         );
    
  2182.         await waitFor(['Count: 1', 'Sync effect']);
    
  2183.         expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  2184.         expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
    
  2185. 
    
  2186.         // This branch enables a feature flag that flushes all passive destroys in a
    
  2187.         // separate pass before flushing any passive creates.
    
  2188.         // A result of this two-pass flush is that an error thrown from unmount does
    
  2189.         // not block the subsequent create functions from being run.
    
  2190.         assertLog(['Oops!', 'Unmount B [0]', 'Mount A [1]', 'Mount B [1]']);
    
  2191.       });
    
  2192. 
    
  2193.       // <Counter> gets unmounted because an error is thrown above.
    
  2194.       // The remaining destroy functions are run later on unmount, since they're passive.
    
  2195.       // In this case, one of them throws again (because of how the test is written).
    
  2196.       assertLog(['Oops!', 'Unmount B [1]']);
    
  2197.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  2198.     });
    
  2199. 
    
  2200.     it('works with memo', async () => {
    
  2201.       function Counter({count}) {
    
  2202.         useLayoutEffect(() => {
    
  2203.           Scheduler.log('Mount: ' + count);
    
  2204.           return () => Scheduler.log('Unmount: ' + count);
    
  2205.         });
    
  2206.         return <Text text={'Count: ' + count} />;
    
  2207.       }
    
  2208.       Counter = memo(Counter);
    
  2209. 
    
  2210.       ReactNoop.render(<Counter count={0} />, () =>
    
  2211.         Scheduler.log('Sync effect'),
    
  2212.       );
    
  2213.       await waitFor(['Count: 0', 'Mount: 0', 'Sync effect']);
    
  2214.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  2215. 
    
  2216.       ReactNoop.render(<Counter count={1} />, () =>
    
  2217.         Scheduler.log('Sync effect'),
    
  2218.       );
    
  2219.       await waitFor(['Count: 1', 'Unmount: 0', 'Mount: 1', 'Sync effect']);
    
  2220.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  2221. 
    
  2222.       ReactNoop.render(null);
    
  2223.       await waitFor(['Unmount: 1']);
    
  2224.       expect(ReactNoop).toMatchRenderedOutput(null);
    
  2225.     });
    
  2226. 
    
  2227.     describe('errors thrown in passive destroy function within unmounted trees', () => {
    
  2228.       let BrokenUseEffectCleanup;
    
  2229.       let ErrorBoundary;
    
  2230.       let LogOnlyErrorBoundary;
    
  2231. 
    
  2232.       beforeEach(() => {
    
  2233.         BrokenUseEffectCleanup = function () {
    
  2234.           useEffect(() => {
    
  2235.             Scheduler.log('BrokenUseEffectCleanup useEffect');
    
  2236.             return () => {
    
  2237.               Scheduler.log('BrokenUseEffectCleanup useEffect destroy');
    
  2238.               throw new Error('Expected error');
    
  2239.             };
    
  2240.           }, []);
    
  2241. 
    
  2242.           return 'inner child';
    
  2243.         };
    
  2244. 
    
  2245.         ErrorBoundary = class extends React.Component {
    
  2246.           state = {error: null};
    
  2247.           static getDerivedStateFromError(error) {
    
  2248.             Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
    
  2249.             return {error};
    
  2250.           }
    
  2251.           componentDidCatch(error, info) {
    
  2252.             Scheduler.log(`ErrorBoundary componentDidCatch`);
    
  2253.           }
    
  2254.           render() {
    
  2255.             if (this.state.error) {
    
  2256.               Scheduler.log('ErrorBoundary render error');
    
  2257.               return <span prop="ErrorBoundary fallback" />;
    
  2258.             }
    
  2259.             Scheduler.log('ErrorBoundary render success');
    
  2260.             return this.props.children || null;
    
  2261.           }
    
  2262.         };
    
  2263. 
    
  2264.         LogOnlyErrorBoundary = class extends React.Component {
    
  2265.           componentDidCatch(error, info) {
    
  2266.             Scheduler.log(`LogOnlyErrorBoundary componentDidCatch`);
    
  2267.           }
    
  2268.           render() {
    
  2269.             Scheduler.log(`LogOnlyErrorBoundary render`);
    
  2270.             return this.props.children || null;
    
  2271.           }
    
  2272.         };
    
  2273.       });
    
  2274. 
    
  2275.       it('should use the nearest still-mounted boundary if there are no unmounted boundaries', async () => {
    
  2276.         await act(() => {
    
  2277.           ReactNoop.render(
    
  2278.             <LogOnlyErrorBoundary>
    
  2279.               <BrokenUseEffectCleanup />
    
  2280.             </LogOnlyErrorBoundary>,
    
  2281.           );
    
  2282.         });
    
  2283. 
    
  2284.         assertLog([
    
  2285.           'LogOnlyErrorBoundary render',
    
  2286.           'BrokenUseEffectCleanup useEffect',
    
  2287.         ]);
    
  2288. 
    
  2289.         await act(() => {
    
  2290.           ReactNoop.render(<LogOnlyErrorBoundary />);
    
  2291.         });
    
  2292. 
    
  2293.         assertLog([
    
  2294.           'LogOnlyErrorBoundary render',
    
  2295.           'BrokenUseEffectCleanup useEffect destroy',
    
  2296.           'LogOnlyErrorBoundary componentDidCatch',
    
  2297.         ]);
    
  2298.       });
    
  2299. 
    
  2300.       it('should skip unmounted boundaries and use the nearest still-mounted boundary', async () => {
    
  2301.         function Conditional({showChildren}) {
    
  2302.           if (showChildren) {
    
  2303.             return (
    
  2304.               <ErrorBoundary>
    
  2305.                 <BrokenUseEffectCleanup />
    
  2306.               </ErrorBoundary>
    
  2307.             );
    
  2308.           } else {
    
  2309.             return null;
    
  2310.           }
    
  2311.         }
    
  2312. 
    
  2313.         await act(() => {
    
  2314.           ReactNoop.render(
    
  2315.             <LogOnlyErrorBoundary>
    
  2316.               <Conditional showChildren={true} />
    
  2317.             </LogOnlyErrorBoundary>,
    
  2318.           );
    
  2319.         });
    
  2320. 
    
  2321.         assertLog([
    
  2322.           'LogOnlyErrorBoundary render',
    
  2323.           'ErrorBoundary render success',
    
  2324.           'BrokenUseEffectCleanup useEffect',
    
  2325.         ]);
    
  2326. 
    
  2327.         await act(() => {
    
  2328.           ReactNoop.render(
    
  2329.             <LogOnlyErrorBoundary>
    
  2330.               <Conditional showChildren={false} />
    
  2331.             </LogOnlyErrorBoundary>,
    
  2332.           );
    
  2333.         });
    
  2334. 
    
  2335.         assertLog([
    
  2336.           'LogOnlyErrorBoundary render',
    
  2337.           'BrokenUseEffectCleanup useEffect destroy',
    
  2338.           'LogOnlyErrorBoundary componentDidCatch',
    
  2339.         ]);
    
  2340.       });
    
  2341. 
    
  2342.       it('should call getDerivedStateFromError in the nearest still-mounted boundary', async () => {
    
  2343.         function Conditional({showChildren}) {
    
  2344.           if (showChildren) {
    
  2345.             return <BrokenUseEffectCleanup />;
    
  2346.           } else {
    
  2347.             return null;
    
  2348.           }
    
  2349.         }
    
  2350. 
    
  2351.         await act(() => {
    
  2352.           ReactNoop.render(
    
  2353.             <ErrorBoundary>
    
  2354.               <Conditional showChildren={true} />
    
  2355.             </ErrorBoundary>,
    
  2356.           );
    
  2357.         });
    
  2358. 
    
  2359.         assertLog([
    
  2360.           'ErrorBoundary render success',
    
  2361.           'BrokenUseEffectCleanup useEffect',
    
  2362.         ]);
    
  2363. 
    
  2364.         await act(() => {
    
  2365.           ReactNoop.render(
    
  2366.             <ErrorBoundary>
    
  2367.               <Conditional showChildren={false} />
    
  2368.             </ErrorBoundary>,
    
  2369.           );
    
  2370.         });
    
  2371. 
    
  2372.         assertLog([
    
  2373.           'ErrorBoundary render success',
    
  2374.           'BrokenUseEffectCleanup useEffect destroy',
    
  2375.           'ErrorBoundary static getDerivedStateFromError',
    
  2376.           'ErrorBoundary render error',
    
  2377.           'ErrorBoundary componentDidCatch',
    
  2378.         ]);
    
  2379. 
    
  2380.         expect(ReactNoop).toMatchRenderedOutput(
    
  2381.           <span prop="ErrorBoundary fallback" />,
    
  2382.         );
    
  2383.       });
    
  2384. 
    
  2385.       it('should rethrow error if there are no still-mounted boundaries', async () => {
    
  2386.         function Conditional({showChildren}) {
    
  2387.           if (showChildren) {
    
  2388.             return (
    
  2389.               <ErrorBoundary>
    
  2390.                 <BrokenUseEffectCleanup />
    
  2391.               </ErrorBoundary>
    
  2392.             );
    
  2393.           } else {
    
  2394.             return null;
    
  2395.           }
    
  2396.         }
    
  2397. 
    
  2398.         await act(() => {
    
  2399.           ReactNoop.render(<Conditional showChildren={true} />);
    
  2400.         });
    
  2401. 
    
  2402.         assertLog([
    
  2403.           'ErrorBoundary render success',
    
  2404.           'BrokenUseEffectCleanup useEffect',
    
  2405.         ]);
    
  2406. 
    
  2407.         await act(async () => {
    
  2408.           ReactNoop.render(<Conditional showChildren={false} />);
    
  2409.           await waitForThrow('Expected error');
    
  2410.         });
    
  2411. 
    
  2412.         assertLog(['BrokenUseEffectCleanup useEffect destroy']);
    
  2413. 
    
  2414.         expect(ReactNoop).toMatchRenderedOutput(null);
    
  2415.       });
    
  2416.     });
    
  2417. 
    
  2418.     it('calls passive effect destroy functions for memoized components', async () => {
    
  2419.       const Wrapper = ({children}) => children;
    
  2420.       function Child() {
    
  2421.         React.useEffect(() => {
    
  2422.           Scheduler.log('passive create');
    
  2423.           return () => {
    
  2424.             Scheduler.log('passive destroy');
    
  2425.           };
    
  2426.         }, []);
    
  2427.         React.useLayoutEffect(() => {
    
  2428.           Scheduler.log('layout create');
    
  2429.           return () => {
    
  2430.             Scheduler.log('layout destroy');
    
  2431.           };
    
  2432.         }, []);
    
  2433.         Scheduler.log('render');
    
  2434.         return null;
    
  2435.       }
    
  2436. 
    
  2437.       const isEqual = (prevProps, nextProps) =>
    
  2438.         prevProps.prop === nextProps.prop;
    
  2439.       const MemoizedChild = React.memo(Child, isEqual);
    
  2440. 
    
  2441.       await act(() => {
    
  2442.         ReactNoop.render(
    
  2443.           <Wrapper>
    
  2444.             <MemoizedChild key={1} />
    
  2445.           </Wrapper>,
    
  2446.         );
    
  2447.       });
    
  2448.       assertLog(['render', 'layout create', 'passive create']);
    
  2449. 
    
  2450.       // Include at least one no-op (memoized) update to trigger original bug.
    
  2451.       await act(() => {
    
  2452.         ReactNoop.render(
    
  2453.           <Wrapper>
    
  2454.             <MemoizedChild key={1} />
    
  2455.           </Wrapper>,
    
  2456.         );
    
  2457.       });
    
  2458.       assertLog([]);
    
  2459. 
    
  2460.       await act(() => {
    
  2461.         ReactNoop.render(
    
  2462.           <Wrapper>
    
  2463.             <MemoizedChild key={2} />
    
  2464.           </Wrapper>,
    
  2465.         );
    
  2466.       });
    
  2467.       assertLog([
    
  2468.         'render',
    
  2469.         'layout destroy',
    
  2470.         'layout create',
    
  2471.         'passive destroy',
    
  2472.         'passive create',
    
  2473.       ]);
    
  2474. 
    
  2475.       await act(() => {
    
  2476.         ReactNoop.render(null);
    
  2477.       });
    
  2478.       assertLog(['layout destroy', 'passive destroy']);
    
  2479.     });
    
  2480. 
    
  2481.     it('calls passive effect destroy functions for descendants of memoized components', async () => {
    
  2482.       const Wrapper = ({children}) => children;
    
  2483.       function Child() {
    
  2484.         return <Grandchild />;
    
  2485.       }
    
  2486. 
    
  2487.       function Grandchild() {
    
  2488.         React.useEffect(() => {
    
  2489.           Scheduler.log('passive create');
    
  2490.           return () => {
    
  2491.             Scheduler.log('passive destroy');
    
  2492.           };
    
  2493.         }, []);
    
  2494.         React.useLayoutEffect(() => {
    
  2495.           Scheduler.log('layout create');
    
  2496.           return () => {
    
  2497.             Scheduler.log('layout destroy');
    
  2498.           };
    
  2499.         }, []);
    
  2500.         Scheduler.log('render');
    
  2501.         return null;
    
  2502.       }
    
  2503. 
    
  2504.       const isEqual = (prevProps, nextProps) =>
    
  2505.         prevProps.prop === nextProps.prop;
    
  2506.       const MemoizedChild = React.memo(Child, isEqual);
    
  2507. 
    
  2508.       await act(() => {
    
  2509.         ReactNoop.render(
    
  2510.           <Wrapper>
    
  2511.             <MemoizedChild key={1} />
    
  2512.           </Wrapper>,
    
  2513.         );
    
  2514.       });
    
  2515.       assertLog(['render', 'layout create', 'passive create']);
    
  2516. 
    
  2517.       // Include at least one no-op (memoized) update to trigger original bug.
    
  2518.       await act(() => {
    
  2519.         ReactNoop.render(
    
  2520.           <Wrapper>
    
  2521.             <MemoizedChild key={1} />
    
  2522.           </Wrapper>,
    
  2523.         );
    
  2524.       });
    
  2525.       assertLog([]);
    
  2526. 
    
  2527.       await act(() => {
    
  2528.         ReactNoop.render(
    
  2529.           <Wrapper>
    
  2530.             <MemoizedChild key={2} />
    
  2531.           </Wrapper>,
    
  2532.         );
    
  2533.       });
    
  2534.       assertLog([
    
  2535.         'render',
    
  2536.         'layout destroy',
    
  2537.         'layout create',
    
  2538.         'passive destroy',
    
  2539.         'passive create',
    
  2540.       ]);
    
  2541. 
    
  2542.       await act(() => {
    
  2543.         ReactNoop.render(null);
    
  2544.       });
    
  2545.       assertLog(['layout destroy', 'passive destroy']);
    
  2546.     });
    
  2547. 
    
  2548.     it('assumes passive effect destroy function is either a function or undefined', async () => {
    
  2549.       function App(props) {
    
  2550.         useEffect(() => {
    
  2551.           return props.return;
    
  2552.         });
    
  2553.         return null;
    
  2554.       }
    
  2555. 
    
  2556.       const root1 = ReactNoop.createRoot();
    
  2557.       await expect(async () => {
    
  2558.         await act(() => {
    
  2559.           root1.render(<App return={17} />);
    
  2560.         });
    
  2561.       }).toErrorDev([
    
  2562.         'Warning: useEffect must not return anything besides a ' +
    
  2563.           'function, which is used for clean-up. You returned: 17',
    
  2564.       ]);
    
  2565. 
    
  2566.       const root2 = ReactNoop.createRoot();
    
  2567.       await expect(async () => {
    
  2568.         await act(() => {
    
  2569.           root2.render(<App return={null} />);
    
  2570.         });
    
  2571.       }).toErrorDev([
    
  2572.         'Warning: useEffect must not return anything besides a ' +
    
  2573.           'function, which is used for clean-up. You returned null. If your ' +
    
  2574.           'effect does not require clean up, return undefined (or nothing).',
    
  2575.       ]);
    
  2576. 
    
  2577.       const root3 = ReactNoop.createRoot();
    
  2578.       await expect(async () => {
    
  2579.         await act(() => {
    
  2580.           root3.render(<App return={Promise.resolve()} />);
    
  2581.         });
    
  2582.       }).toErrorDev([
    
  2583.         'Warning: useEffect must not return anything besides a ' +
    
  2584.           'function, which is used for clean-up.\n\n' +
    
  2585.           'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
    
  2586.       ]);
    
  2587. 
    
  2588.       // Error on unmount because React assumes the value is a function
    
  2589.       await act(async () => {
    
  2590.         root3.render(null);
    
  2591.         await waitForThrow('is not a function');
    
  2592.       });
    
  2593.     });
    
  2594.   });
    
  2595. 
    
  2596.   describe('useInsertionEffect', () => {
    
  2597.     it('fires insertion effects after snapshots on update', async () => {
    
  2598.       function CounterA(props) {
    
  2599.         useInsertionEffect(() => {
    
  2600.           Scheduler.log(`Create insertion`);
    
  2601.           return () => {
    
  2602.             Scheduler.log(`Destroy insertion`);
    
  2603.           };
    
  2604.         });
    
  2605.         return null;
    
  2606.       }
    
  2607. 
    
  2608.       class CounterB extends React.Component {
    
  2609.         getSnapshotBeforeUpdate(prevProps, prevState) {
    
  2610.           Scheduler.log(`Get Snapshot`);
    
  2611.           return null;
    
  2612.         }
    
  2613. 
    
  2614.         componentDidUpdate() {}
    
  2615. 
    
  2616.         render() {
    
  2617.           return null;
    
  2618.         }
    
  2619.       }
    
  2620. 
    
  2621.       await act(async () => {
    
  2622.         ReactNoop.render(
    
  2623.           <>
    
  2624.             <CounterA />
    
  2625.             <CounterB />
    
  2626.           </>,
    
  2627.         );
    
  2628. 
    
  2629.         await waitForAll(['Create insertion']);
    
  2630.       });
    
  2631. 
    
  2632.       // Update
    
  2633.       await act(async () => {
    
  2634.         ReactNoop.render(
    
  2635.           <>
    
  2636.             <CounterA />
    
  2637.             <CounterB />
    
  2638.           </>,
    
  2639.         );
    
  2640. 
    
  2641.         await waitForAll([
    
  2642.           'Get Snapshot',
    
  2643.           'Destroy insertion',
    
  2644.           'Create insertion',
    
  2645.         ]);
    
  2646.       });
    
  2647. 
    
  2648.       // Unmount everything
    
  2649.       await act(async () => {
    
  2650.         ReactNoop.render(null);
    
  2651. 
    
  2652.         await waitForAll(['Destroy insertion']);
    
  2653.       });
    
  2654.     });
    
  2655. 
    
  2656.     it('fires insertion effects before layout effects', async () => {
    
  2657.       let committedText = '(empty)';
    
  2658. 
    
  2659.       function Counter(props) {
    
  2660.         useInsertionEffect(() => {
    
  2661.           Scheduler.log(`Create insertion [current: ${committedText}]`);
    
  2662.           committedText = String(props.count);
    
  2663.           return () => {
    
  2664.             Scheduler.log(`Destroy insertion [current: ${committedText}]`);
    
  2665.           };
    
  2666.         });
    
  2667.         useLayoutEffect(() => {
    
  2668.           Scheduler.log(`Create layout [current: ${committedText}]`);
    
  2669.           return () => {
    
  2670.             Scheduler.log(`Destroy layout [current: ${committedText}]`);
    
  2671.           };
    
  2672.         });
    
  2673.         useEffect(() => {
    
  2674.           Scheduler.log(`Create passive [current: ${committedText}]`);
    
  2675.           return () => {
    
  2676.             Scheduler.log(`Destroy passive [current: ${committedText}]`);
    
  2677.           };
    
  2678.         });
    
  2679.         return null;
    
  2680.       }
    
  2681.       await act(async () => {
    
  2682.         ReactNoop.render(<Counter count={0} />);
    
  2683. 
    
  2684.         await waitForPaint([
    
  2685.           'Create insertion [current: (empty)]',
    
  2686.           'Create layout [current: 0]',
    
  2687.         ]);
    
  2688.         expect(committedText).toEqual('0');
    
  2689.       });
    
  2690. 
    
  2691.       assertLog(['Create passive [current: 0]']);
    
  2692. 
    
  2693.       // Unmount everything
    
  2694.       await act(async () => {
    
  2695.         ReactNoop.render(null);
    
  2696. 
    
  2697.         await waitForPaint([
    
  2698.           'Destroy insertion [current: 0]',
    
  2699.           'Destroy layout [current: 0]',
    
  2700.         ]);
    
  2701.       });
    
  2702. 
    
  2703.       assertLog(['Destroy passive [current: 0]']);
    
  2704.     });
    
  2705. 
    
  2706.     it('force flushes passive effects before firing new insertion effects', async () => {
    
  2707.       let committedText = '(empty)';
    
  2708. 
    
  2709.       function Counter(props) {
    
  2710.         useInsertionEffect(() => {
    
  2711.           Scheduler.log(`Create insertion [current: ${committedText}]`);
    
  2712.           committedText = String(props.count);
    
  2713.           return () => {
    
  2714.             Scheduler.log(`Destroy insertion [current: ${committedText}]`);
    
  2715.           };
    
  2716.         });
    
  2717.         useLayoutEffect(() => {
    
  2718.           Scheduler.log(`Create layout [current: ${committedText}]`);
    
  2719.           committedText = String(props.count);
    
  2720.           return () => {
    
  2721.             Scheduler.log(`Destroy layout [current: ${committedText}]`);
    
  2722.           };
    
  2723.         });
    
  2724.         useEffect(() => {
    
  2725.           Scheduler.log(`Create passive [current: ${committedText}]`);
    
  2726.           return () => {
    
  2727.             Scheduler.log(`Destroy passive [current: ${committedText}]`);
    
  2728.           };
    
  2729.         });
    
  2730.         return null;
    
  2731.       }
    
  2732. 
    
  2733.       await act(async () => {
    
  2734.         React.startTransition(() => {
    
  2735.           ReactNoop.render(<Counter count={0} />);
    
  2736.         });
    
  2737.         await waitForPaint([
    
  2738.           'Create insertion [current: (empty)]',
    
  2739.           'Create layout [current: 0]',
    
  2740.         ]);
    
  2741.         expect(committedText).toEqual('0');
    
  2742. 
    
  2743.         React.startTransition(() => {
    
  2744.           ReactNoop.render(<Counter count={1} />);
    
  2745.         });
    
  2746.         await waitForPaint([
    
  2747.           'Create passive [current: 0]',
    
  2748.           'Destroy insertion [current: 0]',
    
  2749.           'Create insertion [current: 0]',
    
  2750.           'Destroy layout [current: 1]',
    
  2751.           'Create layout [current: 1]',
    
  2752.         ]);
    
  2753.         expect(committedText).toEqual('1');
    
  2754.       });
    
  2755.       assertLog([
    
  2756.         'Destroy passive [current: 1]',
    
  2757.         'Create passive [current: 1]',
    
  2758.       ]);
    
  2759.     });
    
  2760. 
    
  2761.     it('fires all insertion effects (interleaved) before firing any layout effects', async () => {
    
  2762.       let committedA = '(empty)';
    
  2763.       let committedB = '(empty)';
    
  2764. 
    
  2765.       function CounterA(props) {
    
  2766.         useInsertionEffect(() => {
    
  2767.           Scheduler.log(
    
  2768.             `Create Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2769.           );
    
  2770.           committedA = String(props.count);
    
  2771.           return () => {
    
  2772.             Scheduler.log(
    
  2773.               `Destroy Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2774.             );
    
  2775.           };
    
  2776.         });
    
  2777.         useInsertionEffect(() => {
    
  2778.           Scheduler.log(
    
  2779.             `Create Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2780.           );
    
  2781.           committedA = String(props.count);
    
  2782.           return () => {
    
  2783.             Scheduler.log(
    
  2784.               `Destroy Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2785.             );
    
  2786.           };
    
  2787.         });
    
  2788. 
    
  2789.         useLayoutEffect(() => {
    
  2790.           Scheduler.log(
    
  2791.             `Create Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2792.           );
    
  2793.           return () => {
    
  2794.             Scheduler.log(
    
  2795.               `Destroy Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2796.             );
    
  2797.           };
    
  2798.         });
    
  2799. 
    
  2800.         useLayoutEffect(() => {
    
  2801.           Scheduler.log(
    
  2802.             `Create Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2803.           );
    
  2804.           return () => {
    
  2805.             Scheduler.log(
    
  2806.               `Destroy Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
    
  2807.             );
    
  2808.           };
    
  2809.         });
    
  2810.         return null;
    
  2811.       }
    
  2812. 
    
  2813.       function CounterB(props) {
    
  2814.         useInsertionEffect(() => {
    
  2815.           Scheduler.log(
    
  2816.             `Create Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2817.           );
    
  2818.           committedB = String(props.count);
    
  2819.           return () => {
    
  2820.             Scheduler.log(
    
  2821.               `Destroy Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2822.             );
    
  2823.           };
    
  2824.         });
    
  2825.         useInsertionEffect(() => {
    
  2826.           Scheduler.log(
    
  2827.             `Create Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2828.           );
    
  2829.           committedB = String(props.count);
    
  2830.           return () => {
    
  2831.             Scheduler.log(
    
  2832.               `Destroy Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2833.             );
    
  2834.           };
    
  2835.         });
    
  2836. 
    
  2837.         useLayoutEffect(() => {
    
  2838.           Scheduler.log(
    
  2839.             `Create Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2840.           );
    
  2841.           return () => {
    
  2842.             Scheduler.log(
    
  2843.               `Destroy Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2844.             );
    
  2845.           };
    
  2846.         });
    
  2847. 
    
  2848.         useLayoutEffect(() => {
    
  2849.           Scheduler.log(
    
  2850.             `Create Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2851.           );
    
  2852.           return () => {
    
  2853.             Scheduler.log(
    
  2854.               `Destroy Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
    
  2855.             );
    
  2856.           };
    
  2857.         });
    
  2858.         return null;
    
  2859.       }
    
  2860. 
    
  2861.       await act(async () => {
    
  2862.         ReactNoop.render(
    
  2863.           <React.Fragment>
    
  2864.             <CounterA count={0} />
    
  2865.             <CounterB count={0} />
    
  2866.           </React.Fragment>,
    
  2867.         );
    
  2868.         await waitForAll([
    
  2869.           // All insertion effects fire before all layout effects
    
  2870.           'Create Insertion 1 for Component A [A: (empty), B: (empty)]',
    
  2871.           'Create Insertion 2 for Component A [A: 0, B: (empty)]',
    
  2872.           'Create Insertion 1 for Component B [A: 0, B: (empty)]',
    
  2873.           'Create Insertion 2 for Component B [A: 0, B: 0]',
    
  2874.           'Create Layout 1 for Component A [A: 0, B: 0]',
    
  2875.           'Create Layout 2 for Component A [A: 0, B: 0]',
    
  2876.           'Create Layout 1 for Component B [A: 0, B: 0]',
    
  2877.           'Create Layout 2 for Component B [A: 0, B: 0]',
    
  2878.         ]);
    
  2879.         expect([committedA, committedB]).toEqual(['0', '0']);
    
  2880.       });
    
  2881. 
    
  2882.       await act(async () => {
    
  2883.         ReactNoop.render(
    
  2884.           <React.Fragment>
    
  2885.             <CounterA count={1} />
    
  2886.             <CounterB count={1} />
    
  2887.           </React.Fragment>,
    
  2888.         );
    
  2889.         await waitForAll([
    
  2890.           'Destroy Insertion 1 for Component A [A: 0, B: 0]',
    
  2891.           'Destroy Insertion 2 for Component A [A: 0, B: 0]',
    
  2892.           'Create Insertion 1 for Component A [A: 0, B: 0]',
    
  2893.           'Create Insertion 2 for Component A [A: 1, B: 0]',
    
  2894.           'Destroy Layout 1 for Component A [A: 1, B: 0]',
    
  2895.           'Destroy Layout 2 for Component A [A: 1, B: 0]',
    
  2896.           'Destroy Insertion 1 for Component B [A: 1, B: 0]',
    
  2897.           'Destroy Insertion 2 for Component B [A: 1, B: 0]',
    
  2898.           'Create Insertion 1 for Component B [A: 1, B: 0]',
    
  2899.           'Create Insertion 2 for Component B [A: 1, B: 1]',
    
  2900.           'Destroy Layout 1 for Component B [A: 1, B: 1]',
    
  2901.           'Destroy Layout 2 for Component B [A: 1, B: 1]',
    
  2902.           'Create Layout 1 for Component A [A: 1, B: 1]',
    
  2903.           'Create Layout 2 for Component A [A: 1, B: 1]',
    
  2904.           'Create Layout 1 for Component B [A: 1, B: 1]',
    
  2905.           'Create Layout 2 for Component B [A: 1, B: 1]',
    
  2906.         ]);
    
  2907.         expect([committedA, committedB]).toEqual(['1', '1']);
    
  2908. 
    
  2909.         // Unmount everything
    
  2910.         await act(async () => {
    
  2911.           ReactNoop.render(null);
    
  2912. 
    
  2913.           await waitForAll([
    
  2914.             'Destroy Insertion 1 for Component A [A: 1, B: 1]',
    
  2915.             'Destroy Insertion 2 for Component A [A: 1, B: 1]',
    
  2916.             'Destroy Layout 1 for Component A [A: 1, B: 1]',
    
  2917.             'Destroy Layout 2 for Component A [A: 1, B: 1]',
    
  2918.             'Destroy Insertion 1 for Component B [A: 1, B: 1]',
    
  2919.             'Destroy Insertion 2 for Component B [A: 1, B: 1]',
    
  2920.             'Destroy Layout 1 for Component B [A: 1, B: 1]',
    
  2921.             'Destroy Layout 2 for Component B [A: 1, B: 1]',
    
  2922.           ]);
    
  2923.         });
    
  2924.       });
    
  2925.     });
    
  2926. 
    
  2927.     it('assumes insertion effect destroy function is either a function or undefined', async () => {
    
  2928.       function App(props) {
    
  2929.         useInsertionEffect(() => {
    
  2930.           return props.return;
    
  2931.         });
    
  2932.         return null;
    
  2933.       }
    
  2934. 
    
  2935.       const root1 = ReactNoop.createRoot();
    
  2936.       await expect(async () => {
    
  2937.         await act(() => {
    
  2938.           root1.render(<App return={17} />);
    
  2939.         });
    
  2940.       }).toErrorDev([
    
  2941.         'Warning: useInsertionEffect must not return anything besides a ' +
    
  2942.           'function, which is used for clean-up. You returned: 17',
    
  2943.       ]);
    
  2944. 
    
  2945.       const root2 = ReactNoop.createRoot();
    
  2946.       await expect(async () => {
    
  2947.         await act(() => {
    
  2948.           root2.render(<App return={null} />);
    
  2949.         });
    
  2950.       }).toErrorDev([
    
  2951.         'Warning: useInsertionEffect must not return anything besides a ' +
    
  2952.           'function, which is used for clean-up. You returned null. If your ' +
    
  2953.           'effect does not require clean up, return undefined (or nothing).',
    
  2954.       ]);
    
  2955. 
    
  2956.       const root3 = ReactNoop.createRoot();
    
  2957.       await expect(async () => {
    
  2958.         await act(() => {
    
  2959.           root3.render(<App return={Promise.resolve()} />);
    
  2960.         });
    
  2961.       }).toErrorDev([
    
  2962.         'Warning: useInsertionEffect must not return anything besides a ' +
    
  2963.           'function, which is used for clean-up.\n\n' +
    
  2964.           'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
    
  2965.       ]);
    
  2966. 
    
  2967.       // Error on unmount because React assumes the value is a function
    
  2968.       await act(async () => {
    
  2969.         root3.render(null);
    
  2970.         await waitForThrow('is not a function');
    
  2971.       });
    
  2972.     });
    
  2973. 
    
  2974.     it('warns when setState is called from insertion effect setup', async () => {
    
  2975.       function App(props) {
    
  2976.         const [, setX] = useState(0);
    
  2977.         useInsertionEffect(() => {
    
  2978.           setX(1);
    
  2979.           if (props.throw) {
    
  2980.             throw Error('No');
    
  2981.           }
    
  2982.         }, [props.throw]);
    
  2983.         return null;
    
  2984.       }
    
  2985. 
    
  2986.       const root = ReactNoop.createRoot();
    
  2987.       await expect(async () => {
    
  2988.         await act(() => {
    
  2989.           root.render(<App />);
    
  2990.         });
    
  2991.       }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
    
  2992. 
    
  2993.       await act(async () => {
    
  2994.         root.render(<App throw={true} />);
    
  2995.         await waitForThrow('No');
    
  2996.       });
    
  2997. 
    
  2998.       // Should not warn for regular effects after throw.
    
  2999.       function NotInsertion() {
    
  3000.         const [, setX] = useState(0);
    
  3001.         useEffect(() => {
    
  3002.           setX(1);
    
  3003.         }, []);
    
  3004.         return null;
    
  3005.       }
    
  3006.       await act(() => {
    
  3007.         root.render(<NotInsertion />);
    
  3008.       });
    
  3009.     });
    
  3010. 
    
  3011.     it('warns when setState is called from insertion effect cleanup', async () => {
    
  3012.       function App(props) {
    
  3013.         const [, setX] = useState(0);
    
  3014.         useInsertionEffect(() => {
    
  3015.           if (props.throw) {
    
  3016.             throw Error('No');
    
  3017.           }
    
  3018.           return () => {
    
  3019.             setX(1);
    
  3020.           };
    
  3021.         }, [props.throw, props.foo]);
    
  3022.         return null;
    
  3023.       }
    
  3024. 
    
  3025.       const root = ReactNoop.createRoot();
    
  3026.       await act(() => {
    
  3027.         root.render(<App foo="hello" />);
    
  3028.       });
    
  3029.       await expect(async () => {
    
  3030.         await act(() => {
    
  3031.           root.render(<App foo="goodbye" />);
    
  3032.         });
    
  3033.       }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
    
  3034. 
    
  3035.       await act(async () => {
    
  3036.         root.render(<App throw={true} />);
    
  3037.         await waitForThrow('No');
    
  3038.       });
    
  3039. 
    
  3040.       // Should not warn for regular effects after throw.
    
  3041.       function NotInsertion() {
    
  3042.         const [, setX] = useState(0);
    
  3043.         useEffect(() => {
    
  3044.           setX(1);
    
  3045.         }, []);
    
  3046.         return null;
    
  3047.       }
    
  3048.       await act(() => {
    
  3049.         root.render(<NotInsertion />);
    
  3050.       });
    
  3051.     });
    
  3052.   });
    
  3053. 
    
  3054.   describe('useLayoutEffect', () => {
    
  3055.     it('fires layout effects after the host has been mutated', async () => {
    
  3056.       function getCommittedText() {
    
  3057.         const yields = Scheduler.unstable_clearLog();
    
  3058.         const children = ReactNoop.getChildrenAsJSX();
    
  3059.         Scheduler.log(yields);
    
  3060.         if (children === null) {
    
  3061.           return null;
    
  3062.         }
    
  3063.         return children.props.prop;
    
  3064.       }
    
  3065. 
    
  3066.       function Counter(props) {
    
  3067.         useLayoutEffect(() => {
    
  3068.           Scheduler.log(`Current: ${getCommittedText()}`);
    
  3069.         });
    
  3070.         return <Text text={props.count} />;
    
  3071.       }
    
  3072. 
    
  3073.       ReactNoop.render(<Counter count={0} />, () =>
    
  3074.         Scheduler.log('Sync effect'),
    
  3075.       );
    
  3076.       await waitFor([[0], 'Current: 0', 'Sync effect']);
    
  3077.       expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
    
  3078. 
    
  3079.       ReactNoop.render(<Counter count={1} />, () =>
    
  3080.         Scheduler.log('Sync effect'),
    
  3081.       );
    
  3082.       await waitFor([[1], 'Current: 1', 'Sync effect']);
    
  3083.       expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
    
  3084.     });
    
  3085. 
    
  3086.     it('force flushes passive effects before firing new layout effects', async () => {
    
  3087.       let committedText = '(empty)';
    
  3088. 
    
  3089.       function Counter(props) {
    
  3090.         useLayoutEffect(() => {
    
  3091.           // Normally this would go in a mutation effect, but this test
    
  3092.           // intentionally omits a mutation effect.
    
  3093.           committedText = String(props.count);
    
  3094. 
    
  3095.           Scheduler.log(`Mount layout [current: ${committedText}]`);
    
  3096.           return () => {
    
  3097.             Scheduler.log(`Unmount layout [current: ${committedText}]`);
    
  3098.           };
    
  3099.         });
    
  3100.         useEffect(() => {
    
  3101.           Scheduler.log(`Mount normal [current: ${committedText}]`);
    
  3102.           return () => {
    
  3103.             Scheduler.log(`Unmount normal [current: ${committedText}]`);
    
  3104.           };
    
  3105.         });
    
  3106.         return null;
    
  3107.       }
    
  3108. 
    
  3109.       await act(async () => {
    
  3110.         ReactNoop.render(<Counter count={0} />, () =>
    
  3111.           Scheduler.log('Sync effect'),
    
  3112.         );
    
  3113.         await waitFor(['Mount layout [current: 0]', 'Sync effect']);
    
  3114.         expect(committedText).toEqual('0');
    
  3115.         ReactNoop.render(<Counter count={1} />, () =>
    
  3116.           Scheduler.log('Sync effect'),
    
  3117.         );
    
  3118.         await waitFor([
    
  3119.           'Mount normal [current: 0]',
    
  3120.           'Unmount layout [current: 0]',
    
  3121.           'Mount layout [current: 1]',
    
  3122.           'Sync effect',
    
  3123.         ]);
    
  3124.         expect(committedText).toEqual('1');
    
  3125.       });
    
  3126. 
    
  3127.       assertLog(['Unmount normal [current: 1]', 'Mount normal [current: 1]']);
    
  3128.     });
    
  3129. 
    
  3130.     it('catches errors thrown in useLayoutEffect', async () => {
    
  3131.       class ErrorBoundary extends React.Component {
    
  3132.         state = {error: null};
    
  3133.         static getDerivedStateFromError(error) {
    
  3134.           Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
    
  3135.           return {error};
    
  3136.         }
    
  3137.         render() {
    
  3138.           const {children, id, fallbackID} = this.props;
    
  3139.           const {error} = this.state;
    
  3140.           if (error) {
    
  3141.             Scheduler.log(`${id} render error`);
    
  3142.             return <Component id={fallbackID} />;
    
  3143.           }
    
  3144.           Scheduler.log(`${id} render success`);
    
  3145.           return children || null;
    
  3146.         }
    
  3147.       }
    
  3148. 
    
  3149.       function Component({id}) {
    
  3150.         Scheduler.log('Component render ' + id);
    
  3151.         return <span prop={id} />;
    
  3152.       }
    
  3153. 
    
  3154.       function BrokenLayoutEffectDestroy() {
    
  3155.         useLayoutEffect(() => {
    
  3156.           return () => {
    
  3157.             Scheduler.log('BrokenLayoutEffectDestroy useLayoutEffect destroy');
    
  3158.             throw Error('Expected');
    
  3159.           };
    
  3160.         }, []);
    
  3161. 
    
  3162.         Scheduler.log('BrokenLayoutEffectDestroy render');
    
  3163.         return <span prop="broken" />;
    
  3164.       }
    
  3165. 
    
  3166.       ReactNoop.render(
    
  3167.         <ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
    
  3168.           <Component id="sibling" />
    
  3169.           <ErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
    
  3170.             <BrokenLayoutEffectDestroy />
    
  3171.           </ErrorBoundary>
    
  3172.         </ErrorBoundary>,
    
  3173.       );
    
  3174. 
    
  3175.       await waitForAll([
    
  3176.         'OuterBoundary render success',
    
  3177.         'Component render sibling',
    
  3178.         'InnerBoundary render success',
    
  3179.         'BrokenLayoutEffectDestroy render',
    
  3180.       ]);
    
  3181.       expect(ReactNoop).toMatchRenderedOutput(
    
  3182.         <>
    
  3183.           <span prop="sibling" />
    
  3184.           <span prop="broken" />
    
  3185.         </>,
    
  3186.       );
    
  3187. 
    
  3188.       ReactNoop.render(
    
  3189.         <ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
    
  3190.           <Component id="sibling" />
    
  3191.         </ErrorBoundary>,
    
  3192.       );
    
  3193. 
    
  3194.       // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
    
  3195.       await waitForAll([
    
  3196.         'OuterBoundary render success',
    
  3197.         'Component render sibling',
    
  3198.         'BrokenLayoutEffectDestroy useLayoutEffect destroy',
    
  3199.         'ErrorBoundary static getDerivedStateFromError',
    
  3200.         'OuterBoundary render error',
    
  3201.         'Component render OuterFallback',
    
  3202.       ]);
    
  3203.       expect(ReactNoop).toMatchRenderedOutput(<span prop="OuterFallback" />);
    
  3204.     });
    
  3205. 
    
  3206.     it('assumes layout effect destroy function is either a function or undefined', async () => {
    
  3207.       function App(props) {
    
  3208.         useLayoutEffect(() => {
    
  3209.           return props.return;
    
  3210.         });
    
  3211.         return null;
    
  3212.       }
    
  3213. 
    
  3214.       const root1 = ReactNoop.createRoot();
    
  3215.       await expect(async () => {
    
  3216.         await act(() => {
    
  3217.           root1.render(<App return={17} />);
    
  3218.         });
    
  3219.       }).toErrorDev([
    
  3220.         'Warning: useLayoutEffect must not return anything besides a ' +
    
  3221.           'function, which is used for clean-up. You returned: 17',
    
  3222.       ]);
    
  3223. 
    
  3224.       const root2 = ReactNoop.createRoot();
    
  3225.       await expect(async () => {
    
  3226.         await act(() => {
    
  3227.           root2.render(<App return={null} />);
    
  3228.         });
    
  3229.       }).toErrorDev([
    
  3230.         'Warning: useLayoutEffect must not return anything besides a ' +
    
  3231.           'function, which is used for clean-up. You returned null. If your ' +
    
  3232.           'effect does not require clean up, return undefined (or nothing).',
    
  3233.       ]);
    
  3234. 
    
  3235.       const root3 = ReactNoop.createRoot();
    
  3236.       await expect(async () => {
    
  3237.         await act(() => {
    
  3238.           root3.render(<App return={Promise.resolve()} />);
    
  3239.         });
    
  3240.       }).toErrorDev([
    
  3241.         'Warning: useLayoutEffect must not return anything besides a ' +
    
  3242.           'function, which is used for clean-up.\n\n' +
    
  3243.           'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.',
    
  3244.       ]);
    
  3245. 
    
  3246.       // Error on unmount because React assumes the value is a function
    
  3247.       await act(async () => {
    
  3248.         root3.render(null);
    
  3249.         await waitForThrow('is not a function');
    
  3250.       });
    
  3251.     });
    
  3252.   });
    
  3253. 
    
  3254.   describe('useCallback', () => {
    
  3255.     it('memoizes callback by comparing inputs', async () => {
    
  3256.       class IncrementButton extends React.PureComponent {
    
  3257.         increment = () => {
    
  3258.           this.props.increment();
    
  3259.         };
    
  3260.         render() {
    
  3261.           return <Text text="Increment" />;
    
  3262.         }
    
  3263.       }
    
  3264. 
    
  3265.       function Counter({incrementBy}) {
    
  3266.         const [count, updateCount] = useState(0);
    
  3267.         const increment = useCallback(
    
  3268.           () => updateCount(c => c + incrementBy),
    
  3269.           [incrementBy],
    
  3270.         );
    
  3271.         return (
    
  3272.           <>
    
  3273.             <IncrementButton increment={increment} ref={button} />
    
  3274.             <Text text={'Count: ' + count} />
    
  3275.           </>
    
  3276.         );
    
  3277.       }
    
  3278. 
    
  3279.       const button = React.createRef(null);
    
  3280.       ReactNoop.render(<Counter incrementBy={1} />);
    
  3281.       await waitForAll(['Increment', 'Count: 0']);
    
  3282.       expect(ReactNoop).toMatchRenderedOutput(
    
  3283.         <>
    
  3284.           <span prop="Increment" />
    
  3285.           <span prop="Count: 0" />
    
  3286.         </>,
    
  3287.       );
    
  3288. 
    
  3289.       await act(() => button.current.increment());
    
  3290.       assertLog([
    
  3291.         // Button should not re-render, because its props haven't changed
    
  3292.         // 'Increment',
    
  3293.         'Count: 1',
    
  3294.       ]);
    
  3295.       expect(ReactNoop).toMatchRenderedOutput(
    
  3296.         <>
    
  3297.           <span prop="Increment" />
    
  3298.           <span prop="Count: 1" />
    
  3299.         </>,
    
  3300.       );
    
  3301. 
    
  3302.       // Increase the increment amount
    
  3303.       ReactNoop.render(<Counter incrementBy={10} />);
    
  3304.       await waitForAll([
    
  3305.         // Inputs did change this time
    
  3306.         'Increment',
    
  3307.         'Count: 1',
    
  3308.       ]);
    
  3309.       expect(ReactNoop).toMatchRenderedOutput(
    
  3310.         <>
    
  3311.           <span prop="Increment" />
    
  3312.           <span prop="Count: 1" />
    
  3313.         </>,
    
  3314.       );
    
  3315. 
    
  3316.       // Callback should have updated
    
  3317.       await act(() => button.current.increment());
    
  3318.       assertLog(['Count: 11']);
    
  3319.       expect(ReactNoop).toMatchRenderedOutput(
    
  3320.         <>
    
  3321.           <span prop="Increment" />
    
  3322.           <span prop="Count: 11" />
    
  3323.         </>,
    
  3324.       );
    
  3325.     });
    
  3326.   });
    
  3327. 
    
  3328.   describe('useMemo', () => {
    
  3329.     it('memoizes value by comparing to previous inputs', async () => {
    
  3330.       function CapitalizedText(props) {
    
  3331.         const text = props.text;
    
  3332.         const capitalizedText = useMemo(() => {
    
  3333.           Scheduler.log(`Capitalize '${text}'`);
    
  3334.           return text.toUpperCase();
    
  3335.         }, [text]);
    
  3336.         return <Text text={capitalizedText} />;
    
  3337.       }
    
  3338. 
    
  3339.       ReactNoop.render(<CapitalizedText text="hello" />);
    
  3340.       await waitForAll(["Capitalize 'hello'", 'HELLO']);
    
  3341.       expect(ReactNoop).toMatchRenderedOutput(<span prop="HELLO" />);
    
  3342. 
    
  3343.       ReactNoop.render(<CapitalizedText text="hi" />);
    
  3344.       await waitForAll(["Capitalize 'hi'", 'HI']);
    
  3345.       expect(ReactNoop).toMatchRenderedOutput(<span prop="HI" />);
    
  3346. 
    
  3347.       ReactNoop.render(<CapitalizedText text="hi" />);
    
  3348.       await waitForAll(['HI']);
    
  3349.       expect(ReactNoop).toMatchRenderedOutput(<span prop="HI" />);
    
  3350. 
    
  3351.       ReactNoop.render(<CapitalizedText text="goodbye" />);
    
  3352.       await waitForAll(["Capitalize 'goodbye'", 'GOODBYE']);
    
  3353.       expect(ReactNoop).toMatchRenderedOutput(<span prop="GOODBYE" />);
    
  3354.     });
    
  3355. 
    
  3356.     it('always re-computes if no inputs are provided', async () => {
    
  3357.       function LazyCompute(props) {
    
  3358.         const computed = useMemo(props.compute);
    
  3359.         return <Text text={computed} />;
    
  3360.       }
    
  3361. 
    
  3362.       function computeA() {
    
  3363.         Scheduler.log('compute A');
    
  3364.         return 'A';
    
  3365.       }
    
  3366. 
    
  3367.       function computeB() {
    
  3368.         Scheduler.log('compute B');
    
  3369.         return 'B';
    
  3370.       }
    
  3371. 
    
  3372.       ReactNoop.render(<LazyCompute compute={computeA} />);
    
  3373.       await waitForAll(['compute A', 'A']);
    
  3374. 
    
  3375.       ReactNoop.render(<LazyCompute compute={computeA} />);
    
  3376.       await waitForAll(['compute A', 'A']);
    
  3377. 
    
  3378.       ReactNoop.render(<LazyCompute compute={computeA} />);
    
  3379.       await waitForAll(['compute A', 'A']);
    
  3380. 
    
  3381.       ReactNoop.render(<LazyCompute compute={computeB} />);
    
  3382.       await waitForAll(['compute B', 'B']);
    
  3383.     });
    
  3384. 
    
  3385.     it('should not invoke memoized function during re-renders unless inputs change', async () => {
    
  3386.       function LazyCompute(props) {
    
  3387.         const computed = useMemo(
    
  3388.           () => props.compute(props.input),
    
  3389.           [props.input],
    
  3390.         );
    
  3391.         const [count, setCount] = useState(0);
    
  3392.         if (count < 3) {
    
  3393.           setCount(count + 1);
    
  3394.         }
    
  3395.         return <Text text={computed} />;
    
  3396.       }
    
  3397. 
    
  3398.       function compute(val) {
    
  3399.         Scheduler.log('compute ' + val);
    
  3400.         return val;
    
  3401.       }
    
  3402. 
    
  3403.       ReactNoop.render(<LazyCompute compute={compute} input="A" />);
    
  3404.       await waitForAll(['compute A', 'A']);
    
  3405. 
    
  3406.       ReactNoop.render(<LazyCompute compute={compute} input="A" />);
    
  3407.       await waitForAll(['A']);
    
  3408. 
    
  3409.       ReactNoop.render(<LazyCompute compute={compute} input="B" />);
    
  3410.       await waitForAll(['compute B', 'B']);
    
  3411.     });
    
  3412.   });
    
  3413. 
    
  3414.   describe('useImperativeHandle', () => {
    
  3415.     it('does not update when deps are the same', async () => {
    
  3416.       const INCREMENT = 'INCREMENT';
    
  3417. 
    
  3418.       function reducer(state, action) {
    
  3419.         return action === INCREMENT ? state + 1 : state;
    
  3420.       }
    
  3421. 
    
  3422.       function Counter(props, ref) {
    
  3423.         const [count, dispatch] = useReducer(reducer, 0);
    
  3424.         useImperativeHandle(ref, () => ({count, dispatch}), []);
    
  3425.         return <Text text={'Count: ' + count} />;
    
  3426.       }
    
  3427. 
    
  3428.       Counter = forwardRef(Counter);
    
  3429.       const counter = React.createRef(null);
    
  3430.       ReactNoop.render(<Counter ref={counter} />);
    
  3431.       await waitForAll(['Count: 0']);
    
  3432.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  3433.       expect(counter.current.count).toBe(0);
    
  3434. 
    
  3435.       await act(() => {
    
  3436.         counter.current.dispatch(INCREMENT);
    
  3437.       });
    
  3438.       assertLog(['Count: 1']);
    
  3439.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  3440.       // Intentionally not updated because of [] deps:
    
  3441.       expect(counter.current.count).toBe(0);
    
  3442.     });
    
  3443. 
    
  3444.     // Regression test for https://github.com/facebook/react/issues/14782
    
  3445.     it('automatically updates when deps are not specified', async () => {
    
  3446.       const INCREMENT = 'INCREMENT';
    
  3447. 
    
  3448.       function reducer(state, action) {
    
  3449.         return action === INCREMENT ? state + 1 : state;
    
  3450.       }
    
  3451. 
    
  3452.       function Counter(props, ref) {
    
  3453.         const [count, dispatch] = useReducer(reducer, 0);
    
  3454.         useImperativeHandle(ref, () => ({count, dispatch}));
    
  3455.         return <Text text={'Count: ' + count} />;
    
  3456.       }
    
  3457. 
    
  3458.       Counter = forwardRef(Counter);
    
  3459.       const counter = React.createRef(null);
    
  3460.       ReactNoop.render(<Counter ref={counter} />);
    
  3461.       await waitForAll(['Count: 0']);
    
  3462.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  3463.       expect(counter.current.count).toBe(0);
    
  3464. 
    
  3465.       await act(() => {
    
  3466.         counter.current.dispatch(INCREMENT);
    
  3467.       });
    
  3468.       assertLog(['Count: 1']);
    
  3469.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  3470.       expect(counter.current.count).toBe(1);
    
  3471.     });
    
  3472. 
    
  3473.     it('updates when deps are different', async () => {
    
  3474.       const INCREMENT = 'INCREMENT';
    
  3475. 
    
  3476.       function reducer(state, action) {
    
  3477.         return action === INCREMENT ? state + 1 : state;
    
  3478.       }
    
  3479. 
    
  3480.       let totalRefUpdates = 0;
    
  3481.       function Counter(props, ref) {
    
  3482.         const [count, dispatch] = useReducer(reducer, 0);
    
  3483.         useImperativeHandle(
    
  3484.           ref,
    
  3485.           () => {
    
  3486.             totalRefUpdates++;
    
  3487.             return {count, dispatch};
    
  3488.           },
    
  3489.           [count],
    
  3490.         );
    
  3491.         return <Text text={'Count: ' + count} />;
    
  3492.       }
    
  3493. 
    
  3494.       Counter = forwardRef(Counter);
    
  3495.       const counter = React.createRef(null);
    
  3496.       ReactNoop.render(<Counter ref={counter} />);
    
  3497.       await waitForAll(['Count: 0']);
    
  3498.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
    
  3499.       expect(counter.current.count).toBe(0);
    
  3500.       expect(totalRefUpdates).toBe(1);
    
  3501. 
    
  3502.       await act(() => {
    
  3503.         counter.current.dispatch(INCREMENT);
    
  3504.       });
    
  3505.       assertLog(['Count: 1']);
    
  3506.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  3507.       expect(counter.current.count).toBe(1);
    
  3508.       expect(totalRefUpdates).toBe(2);
    
  3509. 
    
  3510.       // Update that doesn't change the ref dependencies
    
  3511.       ReactNoop.render(<Counter ref={counter} />);
    
  3512.       await waitForAll(['Count: 1']);
    
  3513.       expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
    
  3514.       expect(counter.current.count).toBe(1);
    
  3515.       expect(totalRefUpdates).toBe(2); // Should not increase since last time
    
  3516.     });
    
  3517.   });
    
  3518. 
    
  3519.   describe('useTransition', () => {
    
  3520.     it('delays showing loading state until after timeout', async () => {
    
  3521.       let transition;
    
  3522.       function App() {
    
  3523.         const [show, setShow] = useState(false);
    
  3524.         const [isPending, startTransition] = useTransition();
    
  3525.         transition = () => {
    
  3526.           startTransition(() => {
    
  3527.             setShow(true);
    
  3528.           });
    
  3529.         };
    
  3530.         return (
    
  3531.           <Suspense
    
  3532.             fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
    
  3533.             {show ? (
    
  3534.               <AsyncText text={`After... Pending: ${isPending}`} />
    
  3535.             ) : (
    
  3536.               <Text text={`Before... Pending: ${isPending}`} />
    
  3537.             )}
    
  3538.           </Suspense>
    
  3539.         );
    
  3540.       }
    
  3541.       ReactNoop.render(<App />);
    
  3542.       await waitForAll(['Before... Pending: false']);
    
  3543.       expect(ReactNoop).toMatchRenderedOutput(
    
  3544.         <span prop="Before... Pending: false" />,
    
  3545.       );
    
  3546. 
    
  3547.       await act(async () => {
    
  3548.         transition();
    
  3549. 
    
  3550.         await waitForAll([
    
  3551.           'Before... Pending: true',
    
  3552.           'Suspend! [After... Pending: false]',
    
  3553.           'Loading... Pending: false',
    
  3554.         ]);
    
  3555.         expect(ReactNoop).toMatchRenderedOutput(
    
  3556.           <span prop="Before... Pending: true" />,
    
  3557.         );
    
  3558.         Scheduler.unstable_advanceTime(500);
    
  3559.         await advanceTimers(500);
    
  3560. 
    
  3561.         // Even after a long amount of time, we still don't show a placeholder.
    
  3562.         Scheduler.unstable_advanceTime(100000);
    
  3563.         await advanceTimers(100000);
    
  3564.         expect(ReactNoop).toMatchRenderedOutput(
    
  3565.           <span prop="Before... Pending: true" />,
    
  3566.         );
    
  3567. 
    
  3568.         await resolveText('After... Pending: false');
    
  3569.         assertLog(['Promise resolved [After... Pending: false]']);
    
  3570.         await waitForAll(['After... Pending: false']);
    
  3571.         expect(ReactNoop).toMatchRenderedOutput(
    
  3572.           <span prop="After... Pending: false" />,
    
  3573.         );
    
  3574.       });
    
  3575.     });
    
  3576.   });
    
  3577. 
    
  3578.   describe('useDeferredValue', () => {
    
  3579.     it('defers text value', async () => {
    
  3580.       function TextBox({text}) {
    
  3581.         return <AsyncText text={text} />;
    
  3582.       }
    
  3583. 
    
  3584.       let _setText;
    
  3585.       function App() {
    
  3586.         const [text, setText] = useState('A');
    
  3587.         const deferredText = useDeferredValue(text);
    
  3588.         _setText = setText;
    
  3589.         return (
    
  3590.           <>
    
  3591.             <Text text={text} />
    
  3592.             <Suspense fallback={<Text text={'Loading'} />}>
    
  3593.               <TextBox text={deferredText} />
    
  3594.             </Suspense>
    
  3595.           </>
    
  3596.         );
    
  3597.       }
    
  3598. 
    
  3599.       await act(() => {
    
  3600.         ReactNoop.render(<App />);
    
  3601.       });
    
  3602. 
    
  3603.       assertLog(['A', 'Suspend! [A]', 'Loading']);
    
  3604.       expect(ReactNoop).toMatchRenderedOutput(
    
  3605.         <>
    
  3606.           <span prop="A" />
    
  3607.           <span prop="Loading" />
    
  3608.         </>,
    
  3609.       );
    
  3610. 
    
  3611.       await act(() => resolveText('A'));
    
  3612.       assertLog(['Promise resolved [A]', 'A']);
    
  3613.       expect(ReactNoop).toMatchRenderedOutput(
    
  3614.         <>
    
  3615.           <span prop="A" />
    
  3616.           <span prop="A" />
    
  3617.         </>,
    
  3618.       );
    
  3619. 
    
  3620.       await act(async () => {
    
  3621.         _setText('B');
    
  3622.         await waitForAll(['B', 'A', 'B', 'Suspend! [B]', 'Loading']);
    
  3623.         await waitForAll([]);
    
  3624.         expect(ReactNoop).toMatchRenderedOutput(
    
  3625.           <>
    
  3626.             <span prop="B" />
    
  3627.             <span prop="A" />
    
  3628.           </>,
    
  3629.         );
    
  3630.       });
    
  3631. 
    
  3632.       await act(async () => {
    
  3633.         Scheduler.unstable_advanceTime(250);
    
  3634.         await advanceTimers(250);
    
  3635.       });
    
  3636.       assertLog([]);
    
  3637.       expect(ReactNoop).toMatchRenderedOutput(
    
  3638.         <>
    
  3639.           <span prop="B" />
    
  3640.           <span prop="A" />
    
  3641.         </>,
    
  3642.       );
    
  3643. 
    
  3644.       // Even after a long amount of time, we don't show a fallback
    
  3645.       Scheduler.unstable_advanceTime(100000);
    
  3646.       await advanceTimers(100000);
    
  3647.       await waitForAll([]);
    
  3648.       expect(ReactNoop).toMatchRenderedOutput(
    
  3649.         <>
    
  3650.           <span prop="B" />
    
  3651.           <span prop="A" />
    
  3652.         </>,
    
  3653.       );
    
  3654. 
    
  3655.       await act(async () => {
    
  3656.         await resolveText('B');
    
  3657.       });
    
  3658.       assertLog(['Promise resolved [B]', 'B', 'B']);
    
  3659.       expect(ReactNoop).toMatchRenderedOutput(
    
  3660.         <>
    
  3661.           <span prop="B" />
    
  3662.           <span prop="B" />
    
  3663.         </>,
    
  3664.       );
    
  3665.     });
    
  3666.   });
    
  3667. 
    
  3668.   describe('progressive enhancement (not supported)', () => {
    
  3669.     it('mount additional state', async () => {
    
  3670.       let updateA;
    
  3671.       let updateB;
    
  3672.       // let updateC;
    
  3673. 
    
  3674.       function App(props) {
    
  3675.         const [A, _updateA] = useState(0);
    
  3676.         const [B, _updateB] = useState(0);
    
  3677.         updateA = _updateA;
    
  3678.         updateB = _updateB;
    
  3679. 
    
  3680.         let C;
    
  3681.         if (props.loadC) {
    
  3682.           useState(0);
    
  3683.         } else {
    
  3684.           C = '[not loaded]';
    
  3685.         }
    
  3686. 
    
  3687.         return <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
    
  3688.       }
    
  3689. 
    
  3690.       ReactNoop.render(<App loadC={false} />);
    
  3691.       await waitForAll(['A: 0, B: 0, C: [not loaded]']);
    
  3692.       expect(ReactNoop).toMatchRenderedOutput(
    
  3693.         <span prop="A: 0, B: 0, C: [not loaded]" />,
    
  3694.       );
    
  3695. 
    
  3696.       await act(() => {
    
  3697.         updateA(2);
    
  3698.         updateB(3);
    
  3699.       });
    
  3700. 
    
  3701.       assertLog(['A: 2, B: 3, C: [not loaded]']);
    
  3702.       expect(ReactNoop).toMatchRenderedOutput(
    
  3703.         <span prop="A: 2, B: 3, C: [not loaded]" />,
    
  3704.       );
    
  3705. 
    
  3706.       ReactNoop.render(<App loadC={true} />);
    
  3707.       await expect(async () => {
    
  3708.         await waitForThrow(
    
  3709.           'Rendered more hooks than during the previous render.',
    
  3710.         );
    
  3711.         assertLog([]);
    
  3712.       }).toErrorDev([
    
  3713.         'Warning: React has detected a change in the order of Hooks called by App. ' +
    
  3714.           'This will lead to bugs and errors if not fixed. For more information, ' +
    
  3715.           'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' +
    
  3716.           '   Previous render            Next render\n' +
    
  3717.           '   ------------------------------------------------------\n' +
    
  3718.           '1. useState                   useState\n' +
    
  3719.           '2. useState                   useState\n' +
    
  3720.           '3. undefined                  useState\n' +
    
  3721.           '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
    
  3722.       ]);
    
  3723. 
    
  3724.       // Uncomment if/when we support this again
    
  3725.       // expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 0" />]);
    
  3726. 
    
  3727.       // updateC(4);
    
  3728.       // expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);
    
  3729.       // expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 4" />]);
    
  3730.     });
    
  3731. 
    
  3732.     it('unmount state', async () => {
    
  3733.       let updateA;
    
  3734.       let updateB;
    
  3735.       let updateC;
    
  3736. 
    
  3737.       function App(props) {
    
  3738.         const [A, _updateA] = useState(0);
    
  3739.         const [B, _updateB] = useState(0);
    
  3740.         updateA = _updateA;
    
  3741.         updateB = _updateB;
    
  3742. 
    
  3743.         let C;
    
  3744.         if (props.loadC) {
    
  3745.           const [_C, _updateC] = useState(0);
    
  3746.           C = _C;
    
  3747.           updateC = _updateC;
    
  3748.         } else {
    
  3749.           C = '[not loaded]';
    
  3750.         }
    
  3751. 
    
  3752.         return <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
    
  3753.       }
    
  3754. 
    
  3755.       ReactNoop.render(<App loadC={true} />);
    
  3756.       await waitForAll(['A: 0, B: 0, C: 0']);
    
  3757.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 0, B: 0, C: 0" />);
    
  3758.       await act(() => {
    
  3759.         updateA(2);
    
  3760.         updateB(3);
    
  3761.         updateC(4);
    
  3762.       });
    
  3763.       assertLog(['A: 2, B: 3, C: 4']);
    
  3764.       expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 4" />);
    
  3765.       ReactNoop.render(<App loadC={false} />);
    
  3766.       await waitForThrow(
    
  3767.         'Rendered fewer hooks than expected. This may be caused by an ' +
    
  3768.           'accidental early return statement.',
    
  3769.       );
    
  3770.     });
    
  3771. 
    
  3772.     it('unmount effects', async () => {
    
  3773.       function App(props) {
    
  3774.         useEffect(() => {
    
  3775.           Scheduler.log('Mount A');
    
  3776.           return () => {
    
  3777.             Scheduler.log('Unmount A');
    
  3778.           };
    
  3779.         }, []);
    
  3780. 
    
  3781.         if (props.showMore) {
    
  3782.           useEffect(() => {
    
  3783.             Scheduler.log('Mount B');
    
  3784.             return () => {
    
  3785.               Scheduler.log('Unmount B');
    
  3786.             };
    
  3787.           }, []);
    
  3788.         }
    
  3789. 
    
  3790.         return null;
    
  3791.       }
    
  3792. 
    
  3793.       await act(async () => {
    
  3794.         ReactNoop.render(<App showMore={false} />, () =>
    
  3795.           Scheduler.log('Sync effect'),
    
  3796.         );
    
  3797.         await waitFor(['Sync effect']);
    
  3798.       });
    
  3799. 
    
  3800.       assertLog(['Mount A']);
    
  3801. 
    
  3802.       await act(async () => {
    
  3803.         ReactNoop.render(<App showMore={true} />);
    
  3804.         await expect(async () => {
    
  3805.           await waitForThrow(
    
  3806.             'Rendered more hooks than during the previous render.',
    
  3807.           );
    
  3808.           assertLog([]);
    
  3809.         }).toErrorDev([
    
  3810.           'Warning: React has detected a change in the order of Hooks called by App. ' +
    
  3811.             'This will lead to bugs and errors if not fixed. For more information, ' +
    
  3812.             'read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' +
    
  3813.             '   Previous render            Next render\n' +
    
  3814.             '   ------------------------------------------------------\n' +
    
  3815.             '1. useEffect                  useEffect\n' +
    
  3816.             '2. undefined                  useEffect\n' +
    
  3817.             '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
    
  3818.         ]);
    
  3819.       });
    
  3820. 
    
  3821.       // Uncomment if/when we support this again
    
  3822.       // ReactNoop.flushPassiveEffects();
    
  3823.       // expect(Scheduler).toHaveYielded(['Mount B']);
    
  3824. 
    
  3825.       // ReactNoop.render(<App showMore={false} />);
    
  3826.       // expect(Scheduler).toFlushAndThrow(
    
  3827.       //   'Rendered fewer hooks than expected. This may be caused by an ' +
    
  3828.       //     'accidental early return statement.',
    
  3829.       // );
    
  3830.     });
    
  3831.   });
    
  3832. 
    
  3833.   it('useReducer does not eagerly bail out of state updates', async () => {
    
  3834.     // Edge case based on a bug report
    
  3835.     let setCounter;
    
  3836.     function App() {
    
  3837.       const [counter, _setCounter] = useState(1);
    
  3838.       setCounter = _setCounter;
    
  3839.       return <Component count={counter} />;
    
  3840.     }
    
  3841. 
    
  3842.     function Component({count}) {
    
  3843.       const [state, dispatch] = useReducer(() => {
    
  3844.         // This reducer closes over a value from props. If the reducer is not
    
  3845.         // properly updated, the eager reducer will compare to an old value
    
  3846.         // and bail out incorrectly.
    
  3847.         Scheduler.log('Reducer: ' + count);
    
  3848.         return count;
    
  3849.       }, -1);
    
  3850.       useEffect(() => {
    
  3851.         Scheduler.log('Effect: ' + count);
    
  3852.         dispatch();
    
  3853.       }, [count]);
    
  3854.       Scheduler.log('Render: ' + state);
    
  3855.       return count;
    
  3856.     }
    
  3857. 
    
  3858.     await act(async () => {
    
  3859.       ReactNoop.render(<App />);
    
  3860.       await waitForAll(['Render: -1', 'Effect: 1', 'Reducer: 1', 'Render: 1']);
    
  3861.       expect(ReactNoop).toMatchRenderedOutput('1');
    
  3862.     });
    
  3863. 
    
  3864.     await act(() => {
    
  3865.       setCounter(2);
    
  3866.     });
    
  3867.     assertLog(['Render: 1', 'Effect: 2', 'Reducer: 2', 'Render: 2']);
    
  3868.     expect(ReactNoop).toMatchRenderedOutput('2');
    
  3869.   });
    
  3870. 
    
  3871.   it('useReducer does not replay previous no-op actions when other state changes', async () => {
    
  3872.     let increment;
    
  3873.     let setDisabled;
    
  3874. 
    
  3875.     function Counter() {
    
  3876.       const [disabled, _setDisabled] = useState(true);
    
  3877.       const [count, dispatch] = useReducer((state, action) => {
    
  3878.         if (disabled) {
    
  3879.           return state;
    
  3880.         }
    
  3881.         if (action.type === 'increment') {
    
  3882.           return state + 1;
    
  3883.         }
    
  3884.         return state;
    
  3885.       }, 0);
    
  3886. 
    
  3887.       increment = () => dispatch({type: 'increment'});
    
  3888.       setDisabled = _setDisabled;
    
  3889. 
    
  3890.       Scheduler.log('Render disabled: ' + disabled);
    
  3891.       Scheduler.log('Render count: ' + count);
    
  3892.       return count;
    
  3893.     }
    
  3894. 
    
  3895.     ReactNoop.render(<Counter />);
    
  3896.     await waitForAll(['Render disabled: true', 'Render count: 0']);
    
  3897.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3898. 
    
  3899.     await act(() => {
    
  3900.       // These increments should have no effect, since disabled=true
    
  3901.       increment();
    
  3902.       increment();
    
  3903.       increment();
    
  3904.     });
    
  3905.     assertLog(['Render disabled: true', 'Render count: 0']);
    
  3906.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3907. 
    
  3908.     await act(() => {
    
  3909.       // Enabling the updater should *not* replay the previous increment() actions
    
  3910.       setDisabled(false);
    
  3911.     });
    
  3912.     assertLog(['Render disabled: false', 'Render count: 0']);
    
  3913.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3914.   });
    
  3915. 
    
  3916.   it('useReducer does not replay previous no-op actions when props change', async () => {
    
  3917.     let setDisabled;
    
  3918.     let increment;
    
  3919. 
    
  3920.     function Counter({disabled}) {
    
  3921.       const [count, dispatch] = useReducer((state, action) => {
    
  3922.         if (disabled) {
    
  3923.           return state;
    
  3924.         }
    
  3925.         if (action.type === 'increment') {
    
  3926.           return state + 1;
    
  3927.         }
    
  3928.         return state;
    
  3929.       }, 0);
    
  3930. 
    
  3931.       increment = () => dispatch({type: 'increment'});
    
  3932. 
    
  3933.       Scheduler.log('Render count: ' + count);
    
  3934.       return count;
    
  3935.     }
    
  3936. 
    
  3937.     function App() {
    
  3938.       const [disabled, _setDisabled] = useState(true);
    
  3939.       setDisabled = _setDisabled;
    
  3940.       Scheduler.log('Render disabled: ' + disabled);
    
  3941.       return <Counter disabled={disabled} />;
    
  3942.     }
    
  3943. 
    
  3944.     ReactNoop.render(<App />);
    
  3945.     await waitForAll(['Render disabled: true', 'Render count: 0']);
    
  3946.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3947. 
    
  3948.     await act(() => {
    
  3949.       // These increments should have no effect, since disabled=true
    
  3950.       increment();
    
  3951.       increment();
    
  3952.       increment();
    
  3953.     });
    
  3954.     assertLog(['Render count: 0']);
    
  3955.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3956. 
    
  3957.     await act(() => {
    
  3958.       // Enabling the updater should *not* replay the previous increment() actions
    
  3959.       setDisabled(false);
    
  3960.     });
    
  3961.     assertLog(['Render disabled: false', 'Render count: 0']);
    
  3962.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3963.   });
    
  3964. 
    
  3965.   it('useReducer applies potential no-op changes if made relevant by other updates in the batch', async () => {
    
  3966.     let setDisabled;
    
  3967.     let increment;
    
  3968. 
    
  3969.     function Counter({disabled}) {
    
  3970.       const [count, dispatch] = useReducer((state, action) => {
    
  3971.         if (disabled) {
    
  3972.           return state;
    
  3973.         }
    
  3974.         if (action.type === 'increment') {
    
  3975.           return state + 1;
    
  3976.         }
    
  3977.         return state;
    
  3978.       }, 0);
    
  3979. 
    
  3980.       increment = () => dispatch({type: 'increment'});
    
  3981. 
    
  3982.       Scheduler.log('Render count: ' + count);
    
  3983.       return count;
    
  3984.     }
    
  3985. 
    
  3986.     function App() {
    
  3987.       const [disabled, _setDisabled] = useState(true);
    
  3988.       setDisabled = _setDisabled;
    
  3989.       Scheduler.log('Render disabled: ' + disabled);
    
  3990.       return <Counter disabled={disabled} />;
    
  3991.     }
    
  3992. 
    
  3993.     ReactNoop.render(<App />);
    
  3994.     await waitForAll(['Render disabled: true', 'Render count: 0']);
    
  3995.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  3996. 
    
  3997.     await act(() => {
    
  3998.       // Although the increment happens first (and would seem to do nothing since disabled=true),
    
  3999.       // because these calls are in a batch the parent updates first. This should cause the child
    
  4000.       // to re-render with disabled=false and *then* process the increment action, which now
    
  4001.       // increments the count and causes the component output to change.
    
  4002.       increment();
    
  4003.       setDisabled(false);
    
  4004.     });
    
  4005.     assertLog(['Render disabled: false', 'Render count: 1']);
    
  4006.     expect(ReactNoop).toMatchRenderedOutput('1');
    
  4007.   });
    
  4008. 
    
  4009.   // Regression test. Covers a case where an internal state variable
    
  4010.   // (`didReceiveUpdate`) is not reset properly.
    
  4011.   it('state bail out edge case (#16359)', async () => {
    
  4012.     let setCounterA;
    
  4013.     let setCounterB;
    
  4014. 
    
  4015.     function CounterA() {
    
  4016.       const [counter, setCounter] = useState(0);
    
  4017.       setCounterA = setCounter;
    
  4018.       Scheduler.log('Render A: ' + counter);
    
  4019.       useEffect(() => {
    
  4020.         Scheduler.log('Commit A: ' + counter);
    
  4021.       });
    
  4022.       return counter;
    
  4023.     }
    
  4024. 
    
  4025.     function CounterB() {
    
  4026.       const [counter, setCounter] = useState(0);
    
  4027.       setCounterB = setCounter;
    
  4028.       Scheduler.log('Render B: ' + counter);
    
  4029.       useEffect(() => {
    
  4030.         Scheduler.log('Commit B: ' + counter);
    
  4031.       });
    
  4032.       return counter;
    
  4033.     }
    
  4034. 
    
  4035.     const root = ReactNoop.createRoot(null);
    
  4036.     await act(() => {
    
  4037.       root.render(
    
  4038.         <>
    
  4039.           <CounterA />
    
  4040.           <CounterB />
    
  4041.         </>,
    
  4042.       );
    
  4043.     });
    
  4044.     assertLog(['Render A: 0', 'Render B: 0', 'Commit A: 0', 'Commit B: 0']);
    
  4045. 
    
  4046.     await act(() => {
    
  4047.       setCounterA(1);
    
  4048. 
    
  4049.       // In the same batch, update B twice. To trigger the condition we're
    
  4050.       // testing, the first update is necessary to bypass the early
    
  4051.       // bailout optimization.
    
  4052.       setCounterB(1);
    
  4053.       setCounterB(0);
    
  4054.     });
    
  4055.     assertLog([
    
  4056.       'Render A: 1',
    
  4057.       'Render B: 0',
    
  4058.       'Commit A: 1',
    
  4059.       // B should not fire an effect because the update bailed out
    
  4060.       // 'Commit B: 0',
    
  4061.     ]);
    
  4062.   });
    
  4063. 
    
  4064.   it('should update latest rendered reducer when a preceding state receives a render phase update', async () => {
    
  4065.     // Similar to previous test, except using a preceding render phase update
    
  4066.     // instead of new props.
    
  4067.     let dispatch;
    
  4068.     function App() {
    
  4069.       const [step, setStep] = useState(0);
    
  4070.       const [shadow, _dispatch] = useReducer(() => step, step);
    
  4071.       dispatch = _dispatch;
    
  4072. 
    
  4073.       if (step < 5) {
    
  4074.         setStep(step + 1);
    
  4075.       }
    
  4076. 
    
  4077.       Scheduler.log(`Step: ${step}, Shadow: ${shadow}`);
    
  4078.       return shadow;
    
  4079.     }
    
  4080. 
    
  4081.     ReactNoop.render(<App />);
    
  4082.     await waitForAll([
    
  4083.       'Step: 0, Shadow: 0',
    
  4084.       'Step: 1, Shadow: 0',
    
  4085.       'Step: 2, Shadow: 0',
    
  4086.       'Step: 3, Shadow: 0',
    
  4087.       'Step: 4, Shadow: 0',
    
  4088.       'Step: 5, Shadow: 0',
    
  4089.     ]);
    
  4090.     expect(ReactNoop).toMatchRenderedOutput('0');
    
  4091. 
    
  4092.     await act(() => dispatch());
    
  4093.     assertLog(['Step: 5, Shadow: 5']);
    
  4094.     expect(ReactNoop).toMatchRenderedOutput('5');
    
  4095.   });
    
  4096. 
    
  4097.   it('should process the rest pending updates after a render phase update', async () => {
    
  4098.     // Similar to previous test, except using a preceding render phase update
    
  4099.     // instead of new props.
    
  4100.     let updateA;
    
  4101.     let updateC;
    
  4102.     function App() {
    
  4103.       const [a, setA] = useState(false);
    
  4104.       const [b, setB] = useState(false);
    
  4105.       if (a !== b) {
    
  4106.         setB(a);
    
  4107.       }
    
  4108.       // Even though we called setB above,
    
  4109.       // we should still apply the changes to C,
    
  4110.       // during this render pass.
    
  4111.       const [c, setC] = useState(false);
    
  4112.       updateA = setA;
    
  4113.       updateC = setC;
    
  4114.       return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`;
    
  4115.     }
    
  4116. 
    
  4117.     await act(() => ReactNoop.render(<App />));
    
  4118.     expect(ReactNoop).toMatchRenderedOutput('abc');
    
  4119. 
    
  4120.     await act(() => {
    
  4121.       updateA(true);
    
  4122.       // This update should not get dropped.
    
  4123.       updateC(true);
    
  4124.     });
    
  4125.     expect(ReactNoop).toMatchRenderedOutput('ABC');
    
  4126.   });
    
  4127. 
    
  4128.   it("regression test: don't unmount effects on siblings of deleted nodes", async () => {
    
  4129.     const root = ReactNoop.createRoot();
    
  4130. 
    
  4131.     function Child({label}) {
    
  4132.       useLayoutEffect(() => {
    
  4133.         Scheduler.log('Mount layout ' + label);
    
  4134.         return () => {
    
  4135.           Scheduler.log('Unmount layout ' + label);
    
  4136.         };
    
  4137.       }, [label]);
    
  4138.       useEffect(() => {
    
  4139.         Scheduler.log('Mount passive ' + label);
    
  4140.         return () => {
    
  4141.           Scheduler.log('Unmount passive ' + label);
    
  4142.         };
    
  4143.       }, [label]);
    
  4144.       return label;
    
  4145.     }
    
  4146. 
    
  4147.     await act(() => {
    
  4148.       root.render(
    
  4149.         <>
    
  4150.           <Child key="A" label="A" />
    
  4151.           <Child key="B" label="B" />
    
  4152.         </>,
    
  4153.       );
    
  4154.     });
    
  4155.     assertLog([
    
  4156.       'Mount layout A',
    
  4157.       'Mount layout B',
    
  4158.       'Mount passive A',
    
  4159.       'Mount passive B',
    
  4160.     ]);
    
  4161. 
    
  4162.     // Delete A. This should only unmount the effect on A. In the regression,
    
  4163.     // B's effect would also unmount.
    
  4164.     await act(() => {
    
  4165.       root.render(
    
  4166.         <>
    
  4167.           <Child key="B" label="B" />
    
  4168.         </>,
    
  4169.       );
    
  4170.     });
    
  4171.     assertLog(['Unmount layout A', 'Unmount passive A']);
    
  4172. 
    
  4173.     // Now delete and unmount B.
    
  4174.     await act(() => {
    
  4175.       root.render(null);
    
  4176.     });
    
  4177.     assertLog(['Unmount layout B', 'Unmount passive B']);
    
  4178.   });
    
  4179. 
    
  4180.   it('regression: deleting a tree and unmounting its effects after a reorder', async () => {
    
  4181.     const root = ReactNoop.createRoot();
    
  4182. 
    
  4183.     function Child({label}) {
    
  4184.       useEffect(() => {
    
  4185.         Scheduler.log('Mount ' + label);
    
  4186.         return () => {
    
  4187.           Scheduler.log('Unmount ' + label);
    
  4188.         };
    
  4189.       }, [label]);
    
  4190.       return label;
    
  4191.     }
    
  4192. 
    
  4193.     await act(() => {
    
  4194.       root.render(
    
  4195.         <>
    
  4196.           <Child key="A" label="A" />
    
  4197.           <Child key="B" label="B" />
    
  4198.         </>,
    
  4199.       );
    
  4200.     });
    
  4201.     assertLog(['Mount A', 'Mount B']);
    
  4202. 
    
  4203.     await act(() => {
    
  4204.       root.render(
    
  4205.         <>
    
  4206.           <Child key="B" label="B" />
    
  4207.           <Child key="A" label="A" />
    
  4208.         </>,
    
  4209.       );
    
  4210.     });
    
  4211.     assertLog([]);
    
  4212. 
    
  4213.     await act(() => {
    
  4214.       root.render(null);
    
  4215.     });
    
  4216. 
    
  4217.     assertLog([
    
  4218.       'Unmount B',
    
  4219.       // In the regression, the reorder would cause Child A to "forget" that it
    
  4220.       // contains passive effects. Then when we deleted the tree, A's unmount
    
  4221.       // effect would not fire.
    
  4222.       'Unmount A',
    
  4223.     ]);
    
  4224.   });
    
  4225. 
    
  4226.   // @gate enableSuspenseList
    
  4227.   it('regression: SuspenseList causes unmounts to be dropped on deletion', async () => {
    
  4228.     function Row({label}) {
    
  4229.       useEffect(() => {
    
  4230.         Scheduler.log('Mount ' + label);
    
  4231.         return () => {
    
  4232.           Scheduler.log('Unmount ' + label);
    
  4233.         };
    
  4234.       }, [label]);
    
  4235.       return (
    
  4236.         <Suspense fallback="Loading...">
    
  4237.           <AsyncText text={label} />
    
  4238.         </Suspense>
    
  4239.       );
    
  4240.     }
    
  4241. 
    
  4242.     function App() {
    
  4243.       return (
    
  4244.         <SuspenseList revealOrder="together">
    
  4245.           <Row label="A" />
    
  4246.           <Row label="B" />
    
  4247.         </SuspenseList>
    
  4248.       );
    
  4249.     }
    
  4250. 
    
  4251.     const root = ReactNoop.createRoot();
    
  4252.     await act(() => {
    
  4253.       root.render(<App />);
    
  4254.     });
    
  4255.     assertLog(['Suspend! [A]', 'Suspend! [B]', 'Mount A', 'Mount B']);
    
  4256. 
    
  4257.     await act(async () => {
    
  4258.       await resolveText('A');
    
  4259.     });
    
  4260.     assertLog(['Promise resolved [A]', 'A', 'Suspend! [B]']);
    
  4261. 
    
  4262.     await act(() => {
    
  4263.       root.render(null);
    
  4264.     });
    
  4265.     // In the regression, SuspenseList would cause the children to "forget" that
    
  4266.     // it contains passive effects. Then when we deleted the tree, these unmount
    
  4267.     // effects would not fire.
    
  4268.     assertLog(['Unmount A', 'Unmount B']);
    
  4269.   });
    
  4270. 
    
  4271.   it('effect dependencies are persisted after a render phase update', async () => {
    
  4272.     let handleClick;
    
  4273.     function Test() {
    
  4274.       const [count, setCount] = useState(0);
    
  4275. 
    
  4276.       useEffect(() => {
    
  4277.         Scheduler.log(`Effect: ${count}`);
    
  4278.       }, [count]);
    
  4279. 
    
  4280.       if (count > 0) {
    
  4281.         setCount(0);
    
  4282.       }
    
  4283. 
    
  4284.       handleClick = () => setCount(2);
    
  4285. 
    
  4286.       return <Text text={`Render: ${count}`} />;
    
  4287.     }
    
  4288. 
    
  4289.     await act(() => {
    
  4290.       ReactNoop.render(<Test />);
    
  4291.     });
    
  4292. 
    
  4293.     assertLog(['Render: 0', 'Effect: 0']);
    
  4294. 
    
  4295.     await act(() => {
    
  4296.       handleClick();
    
  4297.     });
    
  4298. 
    
  4299.     assertLog(['Render: 0']);
    
  4300. 
    
  4301.     await act(() => {
    
  4302.       handleClick();
    
  4303.     });
    
  4304. 
    
  4305.     assertLog(['Render: 0']);
    
  4306. 
    
  4307.     await act(() => {
    
  4308.       handleClick();
    
  4309.     });
    
  4310. 
    
  4311.     assertLog(['Render: 0']);
    
  4312.   });
    
  4313. });