1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. 'use strict';
    
  12. 
    
  13. let React;
    
  14. let ReactNoop;
    
  15. let Scheduler;
    
  16. let act;
    
  17. let NormalPriority;
    
  18. let IdlePriority;
    
  19. let runWithPriority;
    
  20. let startTransition;
    
  21. let waitForAll;
    
  22. let waitForPaint;
    
  23. let assertLog;
    
  24. let waitFor;
    
  25. 
    
  26. describe('ReactSchedulerIntegration', () => {
    
  27.   beforeEach(() => {
    
  28.     jest.resetModules();
    
  29. 
    
  30.     React = require('react');
    
  31.     ReactNoop = require('react-noop-renderer');
    
  32.     Scheduler = require('scheduler');
    
  33.     act = require('internal-test-utils').act;
    
  34.     NormalPriority = Scheduler.unstable_NormalPriority;
    
  35.     IdlePriority = Scheduler.unstable_IdlePriority;
    
  36.     runWithPriority = Scheduler.unstable_runWithPriority;
    
  37.     startTransition = React.startTransition;
    
  38. 
    
  39.     const InternalTestUtils = require('internal-test-utils');
    
  40.     waitForAll = InternalTestUtils.waitForAll;
    
  41.     waitForPaint = InternalTestUtils.waitForPaint;
    
  42.     assertLog = InternalTestUtils.assertLog;
    
  43.     waitFor = InternalTestUtils.waitFor;
    
  44.   });
    
  45. 
    
  46.   // Note: This is based on a similar component we use in www. We can delete
    
  47.   // once the extra div wrapper is no longer necessary.
    
  48.   function LegacyHiddenDiv({children, mode}) {
    
  49.     return (
    
  50.       <div hidden={mode === 'hidden'}>
    
  51.         <React.unstable_LegacyHidden
    
  52.           mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
    
  53.           {children}
    
  54.         </React.unstable_LegacyHidden>
    
  55.       </div>
    
  56.     );
    
  57.   }
    
  58. 
    
  59.   it('passive effects are called before Normal-pri scheduled in layout effects', async () => {
    
  60.     const {useEffect, useLayoutEffect} = React;
    
  61.     function Effects({step}) {
    
  62.       useLayoutEffect(() => {
    
  63.         Scheduler.log('Layout Effect');
    
  64.         Scheduler.unstable_scheduleCallback(NormalPriority, () =>
    
  65.           Scheduler.log('Scheduled Normal Callback from Layout Effect'),
    
  66.         );
    
  67.       });
    
  68.       useEffect(() => {
    
  69.         Scheduler.log('Passive Effect');
    
  70.       });
    
  71.       return null;
    
  72.     }
    
  73.     function CleanupEffect() {
    
  74.       useLayoutEffect(() => () => {
    
  75.         Scheduler.log('Cleanup Layout Effect');
    
  76.         Scheduler.unstable_scheduleCallback(NormalPriority, () =>
    
  77.           Scheduler.log('Scheduled Normal Callback from Cleanup Layout Effect'),
    
  78.         );
    
  79.       });
    
  80.       return null;
    
  81.     }
    
  82.     await act(() => {
    
  83.       ReactNoop.render(<CleanupEffect />);
    
  84.     });
    
  85.     assertLog([]);
    
  86.     await act(() => {
    
  87.       ReactNoop.render(<Effects />);
    
  88.     });
    
  89.     assertLog([
    
  90.       'Cleanup Layout Effect',
    
  91.       'Layout Effect',
    
  92.       'Passive Effect',
    
  93.       // These callbacks should be scheduled after the passive effects.
    
  94.       'Scheduled Normal Callback from Cleanup Layout Effect',
    
  95.       'Scheduled Normal Callback from Layout Effect',
    
  96.     ]);
    
  97.   });
    
  98. 
    
  99.   it('requests a paint after committing', async () => {
    
  100.     const scheduleCallback = Scheduler.unstable_scheduleCallback;
    
  101. 
    
  102.     const root = ReactNoop.createRoot();
    
  103.     root.render('Initial');
    
  104.     await waitForAll([]);
    
  105.     expect(root).toMatchRenderedOutput('Initial');
    
  106. 
    
  107.     scheduleCallback(NormalPriority, () => Scheduler.log('A'));
    
  108.     scheduleCallback(NormalPriority, () => Scheduler.log('B'));
    
  109.     scheduleCallback(NormalPriority, () => Scheduler.log('C'));
    
  110. 
    
  111.     // Schedule a React render. React will request a paint after committing it.
    
  112.     React.startTransition(() => {
    
  113.       root.render('Update');
    
  114.     });
    
  115. 
    
  116.     // Perform just a little bit of work. By now, the React task will have
    
  117.     // already been scheduled, behind A, B, and C.
    
  118.     await waitFor(['A']);
    
  119. 
    
  120.     // Schedule some additional tasks. These won't fire until after the React
    
  121.     // update has finished.
    
  122.     scheduleCallback(NormalPriority, () => Scheduler.log('D'));
    
  123.     scheduleCallback(NormalPriority, () => Scheduler.log('E'));
    
  124. 
    
  125.     // Flush everything up to the next paint. Should yield after the
    
  126.     // React commit.
    
  127.     await waitForPaint(['B', 'C']);
    
  128.     expect(root).toMatchRenderedOutput('Update');
    
  129. 
    
  130.     // Now flush the rest of the work.
    
  131.     await waitForAll(['D', 'E']);
    
  132.   });
    
  133. 
    
  134.   // @gate www
    
  135.   it('idle updates are not blocked by offscreen work', async () => {
    
  136.     function Text({text}) {
    
  137.       Scheduler.log(text);
    
  138.       return text;
    
  139.     }
    
  140. 
    
  141.     function App({label}) {
    
  142.       return (
    
  143.         <>
    
  144.           <Text text={`Visible: ` + label} />
    
  145.           <LegacyHiddenDiv mode="hidden">
    
  146.             <Text text={`Hidden: ` + label} />
    
  147.           </LegacyHiddenDiv>
    
  148.         </>
    
  149.       );
    
  150.     }
    
  151. 
    
  152.     const root = ReactNoop.createRoot();
    
  153.     await act(async () => {
    
  154.       root.render(<App label="A" />);
    
  155. 
    
  156.       // Commit the visible content
    
  157.       await waitForPaint(['Visible: A']);
    
  158.       expect(root).toMatchRenderedOutput(
    
  159.         <>
    
  160.           Visible: A
    
  161.           <div hidden={true} />
    
  162.         </>,
    
  163.       );
    
  164. 
    
  165.       // Before the hidden content has a chance to render, schedule an
    
  166.       // idle update
    
  167.       runWithPriority(IdlePriority, () => {
    
  168.         root.render(<App label="B" />);
    
  169.       });
    
  170. 
    
  171.       // The next commit should only include the visible content
    
  172.       await waitForPaint(['Visible: B']);
    
  173.       expect(root).toMatchRenderedOutput(
    
  174.         <>
    
  175.           Visible: B
    
  176.           <div hidden={true} />
    
  177.         </>,
    
  178.       );
    
  179.     });
    
  180. 
    
  181.     // The hidden content commits later
    
  182.     assertLog(['Hidden: B']);
    
  183.     expect(root).toMatchRenderedOutput(
    
  184.       <>
    
  185.         Visible: B<div hidden={true}>Hidden: B</div>
    
  186.       </>,
    
  187.     );
    
  188.   });
    
  189. });
    
  190. 
    
  191. describe(
    
  192.   'regression test: does not infinite loop if `shouldYield` returns ' +
    
  193.     'true after a partial tree expires',
    
  194.   () => {
    
  195.     let logDuringShouldYield = false;
    
  196. 
    
  197.     beforeEach(() => {
    
  198.       jest.resetModules();
    
  199. 
    
  200.       jest.mock('scheduler', () => {
    
  201.         const actual = jest.requireActual('scheduler/unstable_mock');
    
  202.         return {
    
  203.           ...actual,
    
  204.           unstable_shouldYield() {
    
  205.             if (logDuringShouldYield) {
    
  206.               actual.log('shouldYield');
    
  207.             }
    
  208.             return actual.unstable_shouldYield();
    
  209.           },
    
  210.         };
    
  211.       });
    
  212. 
    
  213.       React = require('react');
    
  214.       ReactNoop = require('react-noop-renderer');
    
  215.       Scheduler = require('scheduler');
    
  216.       startTransition = React.startTransition;
    
  217. 
    
  218.       const InternalTestUtils = require('internal-test-utils');
    
  219.       waitForAll = InternalTestUtils.waitForAll;
    
  220.       waitForPaint = InternalTestUtils.waitForPaint;
    
  221.       assertLog = InternalTestUtils.assertLog;
    
  222.       waitFor = InternalTestUtils.waitFor;
    
  223.       act = InternalTestUtils.act;
    
  224.     });
    
  225. 
    
  226.     afterEach(() => {
    
  227.       jest.mock('scheduler', () =>
    
  228.         jest.requireActual('scheduler/unstable_mock'),
    
  229.       );
    
  230.     });
    
  231. 
    
  232.     it('using public APIs to trigger real world scenario', async () => {
    
  233.       // This test reproduces a case where React's Scheduler task timed out but
    
  234.       // the `shouldYield` method returned true. The bug was that React fell
    
  235.       // into an infinite loop, because it would enter the work loop then
    
  236.       // immediately yield back to Scheduler.
    
  237.       //
    
  238.       // (The next test in this suite covers the same case. The difference is
    
  239.       // that this test only uses public APIs, whereas the next test mocks
    
  240.       // `shouldYield` to check when it is called.)
    
  241.       function Text({text}) {
    
  242.         return text;
    
  243.       }
    
  244. 
    
  245.       function App({step}) {
    
  246.         return (
    
  247.           <>
    
  248.             <Text text="A" />
    
  249.             <TriggerErstwhileSchedulerBug />
    
  250.             <Text text="B" />
    
  251.             <TriggerErstwhileSchedulerBug />
    
  252.             <Text text="C" />
    
  253.           </>
    
  254.         );
    
  255.       }
    
  256. 
    
  257.       function TriggerErstwhileSchedulerBug() {
    
  258.         // This triggers a once-upon-a-time bug in Scheduler that caused
    
  259.         // `shouldYield` to return true even though the current task expired.
    
  260.         Scheduler.unstable_advanceTime(10000);
    
  261.         Scheduler.unstable_requestPaint();
    
  262.         return null;
    
  263.       }
    
  264. 
    
  265.       await act(async () => {
    
  266.         ReactNoop.render(<App />);
    
  267.         await waitForPaint([]);
    
  268.         await waitForPaint([]);
    
  269.       });
    
  270.     });
    
  271. 
    
  272.     it('mock Scheduler module to check if `shouldYield` is called', async () => {
    
  273.       // This test reproduces a bug where React's Scheduler task timed out but
    
  274.       // the `shouldYield` method returned true. Usually we try not to mock
    
  275.       // internal methods, but I've made an exception here since the point is
    
  276.       // specifically to test that React is resilient to the behavior of a
    
  277.       // Scheduler API. That being said, feel free to rewrite or delete this
    
  278.       // test if/when the API changes.
    
  279.       function Text({text}) {
    
  280.         Scheduler.log(text);
    
  281.         return text;
    
  282.       }
    
  283. 
    
  284.       function App({step}) {
    
  285.         return (
    
  286.           <>
    
  287.             <Text text="A" />
    
  288.             <Text text="B" />
    
  289.             <Text text="C" />
    
  290.           </>
    
  291.         );
    
  292.       }
    
  293. 
    
  294.       await act(async () => {
    
  295.         // Partially render the tree, then yield
    
  296.         startTransition(() => {
    
  297.           ReactNoop.render(<App />);
    
  298.         });
    
  299.         await waitFor(['A']);
    
  300. 
    
  301.         // Start logging whenever shouldYield is called
    
  302.         logDuringShouldYield = true;
    
  303.         // Let's call it once to confirm the mock actually works
    
  304.         await waitFor(['shouldYield']);
    
  305. 
    
  306.         // Expire the task
    
  307.         Scheduler.unstable_advanceTime(10000);
    
  308.         // Scheduling a new update is a trick to force the expiration to kick
    
  309.         // in. We don't check if a update has been starved at the beginning of
    
  310.         // working on it, since there's no point — we're already working on it.
    
  311.         // We only check before yielding to the main thread (to avoid starvation
    
  312.         // by other main thread work) or when receiving an update (to avoid
    
  313.         // starvation by incoming updates).
    
  314.         startTransition(() => {
    
  315.           ReactNoop.render(<App />);
    
  316.         });
    
  317.         // Because the render expired, React should finish the tree without
    
  318.         // consulting `shouldYield` again
    
  319.         await waitFor(['B', 'C']);
    
  320.       });
    
  321.     });
    
  322.   },
    
  323. );
    
  324. 
    
  325. describe('`act` bypasses Scheduler methods completely,', () => {
    
  326.   let infiniteLoopGuard;
    
  327. 
    
  328.   beforeEach(() => {
    
  329.     jest.resetModules();
    
  330. 
    
  331.     infiniteLoopGuard = 0;
    
  332. 
    
  333.     jest.mock('scheduler', () => {
    
  334.       const actual = jest.requireActual('scheduler/unstable_mock');
    
  335.       return {
    
  336.         ...actual,
    
  337.         unstable_shouldYield() {
    
  338.           // This simulates a bug report where `shouldYield` returns true in a
    
  339.           // unit testing environment. Because `act` will keep working until
    
  340.           // there's no more work left, it would fall into an infinite loop.
    
  341.           // The fix is that when performing work inside `act`, we should bypass
    
  342.           // `shouldYield` completely, because we can't trust it to be correct.
    
  343.           if (infiniteLoopGuard++ > 100) {
    
  344.             throw new Error('Detected an infinite loop');
    
  345.           }
    
  346.           return true;
    
  347.         },
    
  348.       };
    
  349.     });
    
  350. 
    
  351.     React = require('react');
    
  352.     ReactNoop = require('react-noop-renderer');
    
  353.     startTransition = React.startTransition;
    
  354.   });
    
  355. 
    
  356.   afterEach(() => {
    
  357.     jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
    
  358.   });
    
  359. 
    
  360.   // @gate __DEV__
    
  361.   it('inside `act`, does not call `shouldYield`, even during a concurrent render', async () => {
    
  362.     function App() {
    
  363.       return (
    
  364.         <>
    
  365.           <div>A</div>
    
  366.           <div>B</div>
    
  367.           <div>C</div>
    
  368.         </>
    
  369.       );
    
  370.     }
    
  371. 
    
  372.     const root = ReactNoop.createRoot();
    
  373.     const publicAct = React.unstable_act;
    
  374.     const prevIsReactActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
    
  375.     try {
    
  376.       global.IS_REACT_ACT_ENVIRONMENT = true;
    
  377.       await publicAct(async () => {
    
  378.         startTransition(() => root.render(<App />));
    
  379.       });
    
  380.     } finally {
    
  381.       global.IS_REACT_ACT_ENVIRONMENT = prevIsReactActEnvironment;
    
  382.     }
    
  383.     expect(root).toMatchRenderedOutput(
    
  384.       <>
    
  385.         <div>A</div>
    
  386.         <div>B</div>
    
  387.         <div>C</div>
    
  388.       </>,
    
  389.     );
    
  390.   });
    
  391. });