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.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let React;
    
  13. let ReactFeatureFlags;
    
  14. let ReactDOM;
    
  15. let ReactDOMClient;
    
  16. let Scheduler;
    
  17. let mockDevToolsHook;
    
  18. let allSchedulerTags;
    
  19. let allSchedulerTypes;
    
  20. let onCommitRootShouldYield;
    
  21. let act;
    
  22. let waitFor;
    
  23. let waitForAll;
    
  24. let assertLog;
    
  25. 
    
  26. describe('updaters', () => {
    
  27.   beforeEach(() => {
    
  28.     jest.resetModules();
    
  29. 
    
  30.     allSchedulerTags = [];
    
  31.     allSchedulerTypes = [];
    
  32. 
    
  33.     onCommitRootShouldYield = true;
    
  34. 
    
  35.     ReactFeatureFlags = require('shared/ReactFeatureFlags');
    
  36.     ReactFeatureFlags.enableUpdaterTracking = true;
    
  37.     ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
    
  38. 
    
  39.     mockDevToolsHook = {
    
  40.       injectInternals: jest.fn(() => {}),
    
  41.       isDevToolsPresent: true,
    
  42.       onCommitRoot: jest.fn(fiberRoot => {
    
  43.         if (onCommitRootShouldYield) {
    
  44.           Scheduler.log('onCommitRoot');
    
  45.         }
    
  46.         const schedulerTags = [];
    
  47.         const schedulerTypes = [];
    
  48.         fiberRoot.memoizedUpdaters.forEach(fiber => {
    
  49.           schedulerTags.push(fiber.tag);
    
  50.           schedulerTypes.push(fiber.elementType);
    
  51.         });
    
  52.         allSchedulerTags.push(schedulerTags);
    
  53.         allSchedulerTypes.push(schedulerTypes);
    
  54.       }),
    
  55.       onCommitUnmount: jest.fn(() => {}),
    
  56.       onPostCommitRoot: jest.fn(() => {}),
    
  57.       onScheduleRoot: jest.fn(() => {}),
    
  58. 
    
  59.       // Profiling APIs
    
  60.       markCommitStarted: jest.fn(() => {}),
    
  61.       markCommitStopped: jest.fn(() => {}),
    
  62.       markComponentRenderStarted: jest.fn(() => {}),
    
  63.       markComponentRenderStopped: jest.fn(() => {}),
    
  64.       markComponentPassiveEffectMountStarted: jest.fn(() => {}),
    
  65.       markComponentPassiveEffectMountStopped: jest.fn(() => {}),
    
  66.       markComponentPassiveEffectUnmountStarted: jest.fn(() => {}),
    
  67.       markComponentPassiveEffectUnmountStopped: jest.fn(() => {}),
    
  68.       markComponentLayoutEffectMountStarted: jest.fn(() => {}),
    
  69.       markComponentLayoutEffectMountStopped: jest.fn(() => {}),
    
  70.       markComponentLayoutEffectUnmountStarted: jest.fn(() => {}),
    
  71.       markComponentLayoutEffectUnmountStopped: jest.fn(() => {}),
    
  72.       markComponentErrored: jest.fn(() => {}),
    
  73.       markComponentSuspended: jest.fn(() => {}),
    
  74.       markLayoutEffectsStarted: jest.fn(() => {}),
    
  75.       markLayoutEffectsStopped: jest.fn(() => {}),
    
  76.       markPassiveEffectsStarted: jest.fn(() => {}),
    
  77.       markPassiveEffectsStopped: jest.fn(() => {}),
    
  78.       markRenderStarted: jest.fn(() => {}),
    
  79.       markRenderYielded: jest.fn(() => {}),
    
  80.       markRenderStopped: jest.fn(() => {}),
    
  81.       markRenderScheduled: jest.fn(() => {}),
    
  82.       markForceUpdateScheduled: jest.fn(() => {}),
    
  83.       markStateUpdateScheduled: jest.fn(() => {}),
    
  84.     };
    
  85. 
    
  86.     jest.mock(
    
  87.       'react-reconciler/src/ReactFiberDevToolsHook',
    
  88.       () => mockDevToolsHook,
    
  89.     );
    
  90. 
    
  91.     React = require('react');
    
  92.     ReactDOM = require('react-dom');
    
  93.     ReactDOMClient = require('react-dom/client');
    
  94.     Scheduler = require('scheduler');
    
  95. 
    
  96.     act = require('internal-test-utils').act;
    
  97. 
    
  98.     const InternalTestUtils = require('internal-test-utils');
    
  99.     waitFor = InternalTestUtils.waitFor;
    
  100.     waitForAll = InternalTestUtils.waitForAll;
    
  101.     assertLog = InternalTestUtils.assertLog;
    
  102.   });
    
  103. 
    
  104.   it('should report the (host) root as the scheduler for root-level render', async () => {
    
  105.     const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
    
  106. 
    
  107.     const Parent = () => <Child />;
    
  108.     const Child = () => null;
    
  109.     const container = document.createElement('div');
    
  110. 
    
  111.     await act(() => {
    
  112.       ReactDOM.render(<Parent />, container);
    
  113.     });
    
  114.     expect(allSchedulerTags).toEqual([[HostRoot]]);
    
  115. 
    
  116.     await act(() => {
    
  117.       ReactDOM.render(<Parent />, container);
    
  118.     });
    
  119.     expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]);
    
  120.   });
    
  121. 
    
  122.   it('should report a function component as the scheduler for a hooks update', async () => {
    
  123.     let scheduleForA = null;
    
  124.     let scheduleForB = null;
    
  125. 
    
  126.     const Parent = () => (
    
  127.       <React.Fragment>
    
  128.         <SchedulingComponentA />
    
  129.         <SchedulingComponentB />
    
  130.       </React.Fragment>
    
  131.     );
    
  132.     const SchedulingComponentA = () => {
    
  133.       const [count, setCount] = React.useState(0);
    
  134.       scheduleForA = () => setCount(prevCount => prevCount + 1);
    
  135.       return <Child count={count} />;
    
  136.     };
    
  137.     const SchedulingComponentB = () => {
    
  138.       const [count, setCount] = React.useState(0);
    
  139.       scheduleForB = () => setCount(prevCount => prevCount + 1);
    
  140.       return <Child count={count} />;
    
  141.     };
    
  142.     const Child = () => null;
    
  143. 
    
  144.     await act(() => {
    
  145.       ReactDOM.render(<Parent />, document.createElement('div'));
    
  146.     });
    
  147.     expect(scheduleForA).not.toBeNull();
    
  148.     expect(scheduleForB).not.toBeNull();
    
  149.     expect(allSchedulerTypes).toEqual([[null]]);
    
  150. 
    
  151.     await act(() => {
    
  152.       scheduleForA();
    
  153.     });
    
  154.     expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]);
    
  155. 
    
  156.     await act(() => {
    
  157.       scheduleForB();
    
  158.     });
    
  159.     expect(allSchedulerTypes).toEqual([
    
  160.       [null],
    
  161.       [SchedulingComponentA],
    
  162.       [SchedulingComponentB],
    
  163.     ]);
    
  164.   });
    
  165. 
    
  166.   it('should report a class component as the scheduler for a setState update', async () => {
    
  167.     const Parent = () => <SchedulingComponent />;
    
  168.     class SchedulingComponent extends React.Component {
    
  169.       state = {};
    
  170.       render() {
    
  171.         instance = this;
    
  172.         return <Child />;
    
  173.       }
    
  174.     }
    
  175.     const Child = () => null;
    
  176.     let instance;
    
  177.     await act(() => {
    
  178.       ReactDOM.render(<Parent />, document.createElement('div'));
    
  179.     });
    
  180.     expect(allSchedulerTypes).toEqual([[null]]);
    
  181. 
    
  182.     expect(instance).not.toBeNull();
    
  183.     await act(() => {
    
  184.       instance.setState({});
    
  185.     });
    
  186.     expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]);
    
  187.   });
    
  188. 
    
  189.   it('should cover cascading updates', async () => {
    
  190.     let triggerActiveCascade = null;
    
  191.     let triggerPassiveCascade = null;
    
  192. 
    
  193.     const Parent = () => <SchedulingComponent />;
    
  194.     const SchedulingComponent = () => {
    
  195.       const [cascade, setCascade] = React.useState(null);
    
  196.       triggerActiveCascade = () => setCascade('active');
    
  197.       triggerPassiveCascade = () => setCascade('passive');
    
  198.       return <CascadingChild cascade={cascade} />;
    
  199.     };
    
  200.     const CascadingChild = ({cascade}) => {
    
  201.       const [count, setCount] = React.useState(0);
    
  202.       Scheduler.log(`CascadingChild ${count}`);
    
  203.       React.useLayoutEffect(() => {
    
  204.         if (cascade === 'active') {
    
  205.           setCount(prevCount => prevCount + 1);
    
  206.         }
    
  207.         return () => {};
    
  208.       }, [cascade]);
    
  209.       React.useEffect(() => {
    
  210.         if (cascade === 'passive') {
    
  211.           setCount(prevCount => prevCount + 1);
    
  212.         }
    
  213.         return () => {};
    
  214.       }, [cascade]);
    
  215.       return count;
    
  216.     };
    
  217. 
    
  218.     const root = ReactDOMClient.createRoot(document.createElement('div'));
    
  219.     await act(async () => {
    
  220.       root.render(<Parent />);
    
  221.       await waitFor(['CascadingChild 0', 'onCommitRoot']);
    
  222.     });
    
  223.     expect(triggerActiveCascade).not.toBeNull();
    
  224.     expect(triggerPassiveCascade).not.toBeNull();
    
  225.     expect(allSchedulerTypes).toEqual([[null]]);
    
  226. 
    
  227.     await act(async () => {
    
  228.       triggerActiveCascade();
    
  229.       await waitFor([
    
  230.         'CascadingChild 0',
    
  231.         'onCommitRoot',
    
  232.         'CascadingChild 1',
    
  233.         'onCommitRoot',
    
  234.       ]);
    
  235.     });
    
  236.     expect(allSchedulerTypes).toEqual([
    
  237.       [null],
    
  238.       [SchedulingComponent],
    
  239.       [CascadingChild],
    
  240.     ]);
    
  241. 
    
  242.     await act(async () => {
    
  243.       triggerPassiveCascade();
    
  244.       await waitFor([
    
  245.         'CascadingChild 1',
    
  246.         'onCommitRoot',
    
  247.         'CascadingChild 2',
    
  248.         'onCommitRoot',
    
  249.       ]);
    
  250.     });
    
  251.     expect(allSchedulerTypes).toEqual([
    
  252.       [null],
    
  253.       [SchedulingComponent],
    
  254.       [CascadingChild],
    
  255.       [SchedulingComponent],
    
  256.       [CascadingChild],
    
  257.     ]);
    
  258. 
    
  259.     // Verify no outstanding flushes
    
  260.     await waitForAll([]);
    
  261.   });
    
  262. 
    
  263.   it('should cover suspense pings', async () => {
    
  264.     let data = null;
    
  265.     let resolver = null;
    
  266.     let promise = null;
    
  267.     const fakeCacheRead = () => {
    
  268.       if (data === null) {
    
  269.         promise = new Promise(resolve => {
    
  270.           resolver = resolvedData => {
    
  271.             data = resolvedData;
    
  272.             resolve(resolvedData);
    
  273.           };
    
  274.         });
    
  275.         throw promise;
    
  276.       } else {
    
  277.         return data;
    
  278.       }
    
  279.     };
    
  280.     const Parent = () => (
    
  281.       <React.Suspense fallback={<Fallback />}>
    
  282.         <Suspender />
    
  283.       </React.Suspense>
    
  284.     );
    
  285.     const Fallback = () => null;
    
  286.     let setShouldSuspend = null;
    
  287.     const Suspender = ({suspend}) => {
    
  288.       const tuple = React.useState(false);
    
  289.       setShouldSuspend = tuple[1];
    
  290.       if (tuple[0] === true) {
    
  291.         return fakeCacheRead();
    
  292.       } else {
    
  293.         return null;
    
  294.       }
    
  295.     };
    
  296. 
    
  297.     await act(() => {
    
  298.       ReactDOM.render(<Parent />, document.createElement('div'));
    
  299.       assertLog(['onCommitRoot']);
    
  300.     });
    
  301.     expect(setShouldSuspend).not.toBeNull();
    
  302.     expect(allSchedulerTypes).toEqual([[null]]);
    
  303. 
    
  304.     await act(() => {
    
  305.       setShouldSuspend(true);
    
  306.     });
    
  307.     assertLog(['onCommitRoot']);
    
  308.     expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
    
  309. 
    
  310.     expect(resolver).not.toBeNull();
    
  311.     await act(() => {
    
  312.       resolver('abc');
    
  313.       return promise;
    
  314.     });
    
  315.     assertLog(['onCommitRoot']);
    
  316.     expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]);
    
  317. 
    
  318.     // Verify no outstanding flushes
    
  319.     await waitForAll([]);
    
  320.   });
    
  321. 
    
  322.   it('should cover error handling', async () => {
    
  323.     let triggerError = null;
    
  324. 
    
  325.     const Parent = () => {
    
  326.       const [shouldError, setShouldError] = React.useState(false);
    
  327.       triggerError = () => setShouldError(true);
    
  328.       return shouldError ? (
    
  329.         <ErrorBoundary>
    
  330.           <BrokenRender />
    
  331.         </ErrorBoundary>
    
  332.       ) : (
    
  333.         <ErrorBoundary>
    
  334.           <Yield value="initial" />
    
  335.         </ErrorBoundary>
    
  336.       );
    
  337.     };
    
  338.     class ErrorBoundary extends React.Component {
    
  339.       state = {error: null};
    
  340.       componentDidCatch(error) {
    
  341.         this.setState({error});
    
  342.       }
    
  343.       render() {
    
  344.         if (this.state.error) {
    
  345.           return <Yield value="error" />;
    
  346.         }
    
  347.         return this.props.children;
    
  348.       }
    
  349.     }
    
  350.     const Yield = ({value}) => {
    
  351.       Scheduler.log(value);
    
  352.       return null;
    
  353.     };
    
  354.     const BrokenRender = () => {
    
  355.       throw new Error('Hello');
    
  356.     };
    
  357. 
    
  358.     const root = ReactDOMClient.createRoot(document.createElement('div'));
    
  359.     await act(() => {
    
  360.       root.render(<Parent shouldError={false} />);
    
  361.     });
    
  362.     assertLog(['initial', 'onCommitRoot']);
    
  363.     expect(triggerError).not.toBeNull();
    
  364. 
    
  365.     allSchedulerTypes.splice(0);
    
  366.     onCommitRootShouldYield = true;
    
  367. 
    
  368.     await act(() => {
    
  369.       triggerError();
    
  370.     });
    
  371.     assertLog(['onCommitRoot', 'error', 'onCommitRoot']);
    
  372.     expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
    
  373. 
    
  374.     // Verify no outstanding flushes
    
  375.     await waitForAll([]);
    
  376.   });
    
  377. 
    
  378.   it('should distinguish between updaters in the case of interleaved work', async () => {
    
  379.     const {
    
  380.       FunctionComponent,
    
  381.       HostRoot,
    
  382.     } = require('react-reconciler/src/ReactWorkTags');
    
  383. 
    
  384.     let triggerLowPriorityUpdate = null;
    
  385.     let triggerSyncPriorityUpdate = null;
    
  386. 
    
  387.     const SyncPriorityUpdater = () => {
    
  388.       const [count, setCount] = React.useState(0);
    
  389.       triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1);
    
  390.       Scheduler.log(`SyncPriorityUpdater ${count}`);
    
  391.       return <Yield value={`HighPriority ${count}`} />;
    
  392.     };
    
  393.     const LowPriorityUpdater = () => {
    
  394.       const [count, setCount] = React.useState(0);
    
  395.       triggerLowPriorityUpdate = () => {
    
  396.         React.startTransition(() => {
    
  397.           setCount(prevCount => prevCount + 1);
    
  398.         });
    
  399.       };
    
  400.       Scheduler.log(`LowPriorityUpdater ${count}`);
    
  401.       return <Yield value={`LowPriority ${count}`} />;
    
  402.     };
    
  403.     const Yield = ({value}) => {
    
  404.       Scheduler.log(`Yield ${value}`);
    
  405.       return null;
    
  406.     };
    
  407. 
    
  408.     const root = ReactDOMClient.createRoot(document.createElement('div'));
    
  409.     root.render(
    
  410.       <React.Fragment>
    
  411.         <SyncPriorityUpdater />
    
  412.         <LowPriorityUpdater />
    
  413.       </React.Fragment>,
    
  414.     );
    
  415. 
    
  416.     // Render everything initially.
    
  417.     await waitForAll([
    
  418.       'SyncPriorityUpdater 0',
    
  419.       'Yield HighPriority 0',
    
  420.       'LowPriorityUpdater 0',
    
  421.       'Yield LowPriority 0',
    
  422.       'onCommitRoot',
    
  423.     ]);
    
  424.     expect(triggerLowPriorityUpdate).not.toBeNull();
    
  425.     expect(triggerSyncPriorityUpdate).not.toBeNull();
    
  426.     expect(allSchedulerTags).toEqual([[HostRoot]]);
    
  427. 
    
  428.     // Render a partial update, but don't finish.
    
  429.     await act(async () => {
    
  430.       triggerLowPriorityUpdate();
    
  431.       await waitFor(['LowPriorityUpdater 1']);
    
  432.       expect(allSchedulerTags).toEqual([[HostRoot]]);
    
  433. 
    
  434.       // Interrupt with higher priority work.
    
  435.       ReactDOM.flushSync(triggerSyncPriorityUpdate);
    
  436.       assertLog([
    
  437.         'SyncPriorityUpdater 1',
    
  438.         'Yield HighPriority 1',
    
  439.         'onCommitRoot',
    
  440.       ]);
    
  441.       expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]);
    
  442. 
    
  443.       // Finish the initial partial update
    
  444.       triggerLowPriorityUpdate();
    
  445.       await waitForAll([
    
  446.         'LowPriorityUpdater 2',
    
  447.         'Yield LowPriority 2',
    
  448.         'onCommitRoot',
    
  449.       ]);
    
  450.     });
    
  451.     expect(allSchedulerTags).toEqual([
    
  452.       [HostRoot],
    
  453.       [FunctionComponent],
    
  454.       [FunctionComponent],
    
  455.     ]);
    
  456.     expect(allSchedulerTypes).toEqual([
    
  457.       [null],
    
  458.       [SyncPriorityUpdater],
    
  459.       [LowPriorityUpdater],
    
  460.     ]);
    
  461. 
    
  462.     // Verify no outstanding flushes
    
  463.     await waitForAll([]);
    
  464.   });
    
  465. });