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 waitForAll;
    
  18. let waitFor;
    
  19. let assertLog;
    
  20. let waitForPaint;
    
  21. 
    
  22. describe('ReactIncrementalScheduling', () => {
    
  23.   beforeEach(() => {
    
  24.     jest.resetModules();
    
  25. 
    
  26.     React = require('react');
    
  27.     ReactNoop = require('react-noop-renderer');
    
  28.     Scheduler = require('scheduler');
    
  29.     act = require('internal-test-utils').act;
    
  30. 
    
  31.     const InternalTestUtils = require('internal-test-utils');
    
  32.     waitForAll = InternalTestUtils.waitForAll;
    
  33.     waitFor = InternalTestUtils.waitFor;
    
  34.     assertLog = InternalTestUtils.assertLog;
    
  35.     waitForPaint = InternalTestUtils.waitForPaint;
    
  36.   });
    
  37. 
    
  38.   it('schedules and flushes deferred work', async () => {
    
  39.     ReactNoop.render(<span prop="1" />);
    
  40.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  41. 
    
  42.     await waitForAll([]);
    
  43.     expect(ReactNoop).toMatchRenderedOutput(<span prop="1" />);
    
  44.   });
    
  45. 
    
  46.   it('searches for work on other roots once the current root completes', async () => {
    
  47.     ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
    
  48.     ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
    
  49.     ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
    
  50. 
    
  51.     await waitForAll([]);
    
  52. 
    
  53.     expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:1" />);
    
  54.     expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
    
  55.     expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:1" />);
    
  56.   });
    
  57. 
    
  58.   it('schedules top-level updates in order of priority', async () => {
    
  59.     // Initial render.
    
  60.     ReactNoop.render(<span prop={1} />);
    
  61.     await waitForAll([]);
    
  62.     expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
    
  63. 
    
  64.     ReactNoop.batchedUpdates(() => {
    
  65.       ReactNoop.render(<span prop={5} />);
    
  66.       ReactNoop.flushSync(() => {
    
  67.         ReactNoop.render(<span prop={2} />);
    
  68.         ReactNoop.render(<span prop={3} />);
    
  69.         ReactNoop.render(<span prop={4} />);
    
  70.       });
    
  71.     });
    
  72.     // The sync updates flush first.
    
  73.     expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
    
  74. 
    
  75.     // The terminal value should be the last update that was scheduled,
    
  76.     // regardless of priority. In this case, that's the last sync update.
    
  77.     await waitForAll([]);
    
  78.     expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
    
  79.   });
    
  80. 
    
  81.   it('schedules top-level updates with same priority in order of insertion', async () => {
    
  82.     // Initial render.
    
  83.     ReactNoop.render(<span prop={1} />);
    
  84.     await waitForAll([]);
    
  85.     expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
    
  86. 
    
  87.     ReactNoop.render(<span prop={2} />);
    
  88.     ReactNoop.render(<span prop={3} />);
    
  89.     ReactNoop.render(<span prop={4} />);
    
  90.     ReactNoop.render(<span prop={5} />);
    
  91. 
    
  92.     await waitForAll([]);
    
  93.     expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
    
  94.   });
    
  95. 
    
  96.   it('works on deferred roots in the order they were scheduled', async () => {
    
  97.     const {useEffect} = React;
    
  98.     function Text({text}) {
    
  99.       useEffect(() => {
    
  100.         Scheduler.log(text);
    
  101.       }, [text]);
    
  102.       return text;
    
  103.     }
    
  104. 
    
  105.     await act(() => {
    
  106.       ReactNoop.renderToRootWithID(<Text text="a:1" />, 'a');
    
  107.       ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
    
  108.       ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
    
  109.     });
    
  110.     assertLog(['a:1', 'b:1', 'c:1']);
    
  111. 
    
  112.     expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
    
  113.     expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
    
  114.     expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
    
  115. 
    
  116.     // Schedule deferred work in the reverse order
    
  117.     await act(async () => {
    
  118.       React.startTransition(() => {
    
  119.         ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
    
  120.         ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
    
  121.       });
    
  122.       // Ensure it starts in the order it was scheduled
    
  123.       await waitFor(['c:2']);
    
  124. 
    
  125.       expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
    
  126.       expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
    
  127.       expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
    
  128.       // Schedule last bit of work, it will get processed the last
    
  129. 
    
  130.       React.startTransition(() => {
    
  131.         ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
    
  132.       });
    
  133. 
    
  134.       // Keep performing work in the order it was scheduled
    
  135.       await waitFor(['b:2']);
    
  136.       expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
    
  137.       expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
    
  138.       expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
    
  139. 
    
  140.       await waitFor(['a:2']);
    
  141.       expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
    
  142.       expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
    
  143.       expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
    
  144.     });
    
  145.   });
    
  146. 
    
  147.   it('schedules sync updates when inside componentDidMount/Update', async () => {
    
  148.     let instance;
    
  149. 
    
  150.     class Foo extends React.Component {
    
  151.       state = {tick: 0};
    
  152. 
    
  153.       componentDidMount() {
    
  154.         Scheduler.log(
    
  155.           'componentDidMount (before setState): ' + this.state.tick,
    
  156.         );
    
  157.         this.setState({tick: 1});
    
  158.         // We're in a batch. Update hasn't flushed yet.
    
  159.         Scheduler.log('componentDidMount (after setState): ' + this.state.tick);
    
  160.       }
    
  161. 
    
  162.       componentDidUpdate() {
    
  163.         Scheduler.log('componentDidUpdate: ' + this.state.tick);
    
  164.         if (this.state.tick === 2) {
    
  165.           Scheduler.log(
    
  166.             'componentDidUpdate (before setState): ' + this.state.tick,
    
  167.           );
    
  168.           this.setState({tick: 3});
    
  169.           Scheduler.log(
    
  170.             'componentDidUpdate (after setState): ' + this.state.tick,
    
  171.           );
    
  172.           // We're in a batch. Update hasn't flushed yet.
    
  173.         }
    
  174.       }
    
  175. 
    
  176.       render() {
    
  177.         Scheduler.log('render: ' + this.state.tick);
    
  178.         instance = this;
    
  179.         return <span prop={this.state.tick} />;
    
  180.       }
    
  181.     }
    
  182. 
    
  183.     React.startTransition(() => {
    
  184.       ReactNoop.render(<Foo />);
    
  185.     });
    
  186.     // Render without committing
    
  187.     await waitFor(['render: 0']);
    
  188. 
    
  189.     // Do one more unit of work to commit
    
  190.     expect(ReactNoop.flushNextYield()).toEqual([
    
  191.       'componentDidMount (before setState): 0',
    
  192.       'componentDidMount (after setState): 0',
    
  193.       // If the setState inside componentDidMount were deferred, there would be
    
  194.       // no more ops. Because it has Task priority, we get these ops, too:
    
  195.       'render: 1',
    
  196.       'componentDidUpdate: 1',
    
  197.     ]);
    
  198. 
    
  199.     React.startTransition(() => {
    
  200.       instance.setState({tick: 2});
    
  201.     });
    
  202.     await waitFor(['render: 2']);
    
  203.     expect(ReactNoop.flushNextYield()).toEqual([
    
  204.       'componentDidUpdate: 2',
    
  205.       'componentDidUpdate (before setState): 2',
    
  206.       'componentDidUpdate (after setState): 2',
    
  207.       // If the setState inside componentDidUpdate were deferred, there would be
    
  208.       // no more ops. Because it has Task priority, we get these ops, too:
    
  209.       'render: 3',
    
  210.       'componentDidUpdate: 3',
    
  211.     ]);
    
  212.   });
    
  213. 
    
  214.   it('can opt-in to async scheduling inside componentDidMount/Update', async () => {
    
  215.     let instance;
    
  216.     class Foo extends React.Component {
    
  217.       state = {tick: 0};
    
  218. 
    
  219.       componentDidMount() {
    
  220.         React.startTransition(() => {
    
  221.           Scheduler.log(
    
  222.             'componentDidMount (before setState): ' + this.state.tick,
    
  223.           );
    
  224.           this.setState({tick: 1});
    
  225.           Scheduler.log(
    
  226.             'componentDidMount (after setState): ' + this.state.tick,
    
  227.           );
    
  228.         });
    
  229.       }
    
  230. 
    
  231.       componentDidUpdate() {
    
  232.         React.startTransition(() => {
    
  233.           Scheduler.log('componentDidUpdate: ' + this.state.tick);
    
  234.           if (this.state.tick === 2) {
    
  235.             Scheduler.log(
    
  236.               'componentDidUpdate (before setState): ' + this.state.tick,
    
  237.             );
    
  238.             this.setState({tick: 3});
    
  239.             Scheduler.log(
    
  240.               'componentDidUpdate (after setState): ' + this.state.tick,
    
  241.             );
    
  242.           }
    
  243.         });
    
  244.       }
    
  245. 
    
  246.       render() {
    
  247.         Scheduler.log('render: ' + this.state.tick);
    
  248.         instance = this;
    
  249.         return <span prop={this.state.tick} />;
    
  250.       }
    
  251.     }
    
  252. 
    
  253.     ReactNoop.flushSync(() => {
    
  254.       ReactNoop.render(<Foo />);
    
  255.     });
    
  256.     // The cDM update should not have flushed yet because it has async priority.
    
  257.     assertLog([
    
  258.       'render: 0',
    
  259.       'componentDidMount (before setState): 0',
    
  260.       'componentDidMount (after setState): 0',
    
  261.     ]);
    
  262.     expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
    
  263. 
    
  264.     // Now flush the cDM update.
    
  265.     await waitForAll(['render: 1', 'componentDidUpdate: 1']);
    
  266.     expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
    
  267. 
    
  268.     React.startTransition(() => {
    
  269.       instance.setState({tick: 2});
    
  270.     });
    
  271. 
    
  272.     await waitForPaint([
    
  273.       'render: 2',
    
  274.       'componentDidUpdate: 2',
    
  275.       'componentDidUpdate (before setState): 2',
    
  276.       'componentDidUpdate (after setState): 2',
    
  277.     ]);
    
  278.     expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
    
  279. 
    
  280.     // Now flush the cDU update.
    
  281.     await waitForAll(['render: 3', 'componentDidUpdate: 3']);
    
  282.     expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
    
  283.   });
    
  284. 
    
  285.   it('performs Task work even after time runs out', async () => {
    
  286.     class Foo extends React.Component {
    
  287.       state = {step: 1};
    
  288.       componentDidMount() {
    
  289.         this.setState({step: 2}, () => {
    
  290.           this.setState({step: 3}, () => {
    
  291.             this.setState({step: 4}, () => {
    
  292.               this.setState({step: 5});
    
  293.             });
    
  294.           });
    
  295.         });
    
  296.       }
    
  297.       render() {
    
  298.         Scheduler.log('Foo');
    
  299.         return <span prop={this.state.step} />;
    
  300.       }
    
  301.     }
    
  302.     React.startTransition(() => {
    
  303.       ReactNoop.render(<Foo />);
    
  304.     });
    
  305. 
    
  306.     // This should be just enough to complete all the work, but not enough to
    
  307.     // commit it.
    
  308.     await waitFor(['Foo']);
    
  309.     expect(ReactNoop).toMatchRenderedOutput(null);
    
  310. 
    
  311.     // Do one more unit of work.
    
  312.     ReactNoop.flushNextYield();
    
  313.     // The updates should all be flushed with Task priority
    
  314.     expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
    
  315.   });
    
  316. });