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 waitFor;
    
  17. let waitForAll;
    
  18. 
    
  19. describe('ReactIncrementalReflection', () => {
    
  20.   beforeEach(() => {
    
  21.     jest.resetModules();
    
  22. 
    
  23.     React = require('react');
    
  24.     ReactNoop = require('react-noop-renderer');
    
  25.     Scheduler = require('scheduler');
    
  26. 
    
  27.     const InternalTestUtils = require('internal-test-utils');
    
  28.     waitFor = InternalTestUtils.waitFor;
    
  29.     waitForAll = InternalTestUtils.waitForAll;
    
  30.   });
    
  31. 
    
  32.   function div(...children) {
    
  33.     children = children.map(c =>
    
  34.       typeof c === 'string' ? {text: c, hidden: false} : c,
    
  35.     );
    
  36.     return {type: 'div', children, prop: undefined, hidden: false};
    
  37.   }
    
  38. 
    
  39.   function span(prop) {
    
  40.     return {type: 'span', children: [], prop, hidden: false};
    
  41.   }
    
  42. 
    
  43.   it('handles isMounted even when the initial render is deferred', async () => {
    
  44.     const instances = [];
    
  45. 
    
  46.     class Component extends React.Component {
    
  47.       _isMounted() {
    
  48.         // No longer a public API, but we can test that it works internally by
    
  49.         // reaching into the updater.
    
  50.         return this.updater.isMounted(this);
    
  51.       }
    
  52.       UNSAFE_componentWillMount() {
    
  53.         instances.push(this);
    
  54.         Scheduler.log('componentWillMount: ' + this._isMounted());
    
  55.       }
    
  56.       componentDidMount() {
    
  57.         Scheduler.log('componentDidMount: ' + this._isMounted());
    
  58.       }
    
  59.       render() {
    
  60.         return <span />;
    
  61.       }
    
  62.     }
    
  63. 
    
  64.     function Foo() {
    
  65.       return <Component />;
    
  66.     }
    
  67. 
    
  68.     React.startTransition(() => {
    
  69.       ReactNoop.render(<Foo />);
    
  70.     });
    
  71. 
    
  72.     // Render part way through but don't yet commit the updates.
    
  73.     await waitFor(['componentWillMount: false']);
    
  74. 
    
  75.     expect(instances[0]._isMounted()).toBe(false);
    
  76. 
    
  77.     // Render the rest and commit the updates.
    
  78.     await waitForAll(['componentDidMount: true']);
    
  79. 
    
  80.     expect(instances[0]._isMounted()).toBe(true);
    
  81.   });
    
  82. 
    
  83.   it('handles isMounted when an unmount is deferred', async () => {
    
  84.     const instances = [];
    
  85. 
    
  86.     class Component extends React.Component {
    
  87.       _isMounted() {
    
  88.         return this.updater.isMounted(this);
    
  89.       }
    
  90.       UNSAFE_componentWillMount() {
    
  91.         instances.push(this);
    
  92.       }
    
  93.       componentWillUnmount() {
    
  94.         Scheduler.log('componentWillUnmount: ' + this._isMounted());
    
  95.       }
    
  96.       render() {
    
  97.         Scheduler.log('Component');
    
  98.         return <span />;
    
  99.       }
    
  100.     }
    
  101. 
    
  102.     function Other() {
    
  103.       Scheduler.log('Other');
    
  104.       return <span />;
    
  105.     }
    
  106. 
    
  107.     function Foo(props) {
    
  108.       return props.mount ? <Component /> : <Other />;
    
  109.     }
    
  110. 
    
  111.     ReactNoop.render(<Foo mount={true} />);
    
  112.     await waitForAll(['Component']);
    
  113. 
    
  114.     expect(instances[0]._isMounted()).toBe(true);
    
  115. 
    
  116.     React.startTransition(() => {
    
  117.       ReactNoop.render(<Foo mount={false} />);
    
  118.     });
    
  119.     // Render part way through but don't yet commit the updates so it is not
    
  120.     // fully unmounted yet.
    
  121.     await waitFor(['Other']);
    
  122. 
    
  123.     expect(instances[0]._isMounted()).toBe(true);
    
  124. 
    
  125.     // Finish flushing the unmount.
    
  126.     await waitForAll(['componentWillUnmount: true']);
    
  127. 
    
  128.     expect(instances[0]._isMounted()).toBe(false);
    
  129.   });
    
  130. 
    
  131.   it('finds no node before insertion and correct node before deletion', async () => {
    
  132.     let classInstance = null;
    
  133. 
    
  134.     function findInstance(inst) {
    
  135.       // We ignore warnings fired by findInstance because we are testing
    
  136.       // that the actual behavior still works as expected even though it
    
  137.       // is deprecated.
    
  138.       const oldConsoleError = console.error;
    
  139.       console.error = jest.fn();
    
  140.       try {
    
  141.         return ReactNoop.findInstance(inst);
    
  142.       } finally {
    
  143.         console.error = oldConsoleError;
    
  144.       }
    
  145.     }
    
  146. 
    
  147.     class Component extends React.Component {
    
  148.       UNSAFE_componentWillMount() {
    
  149.         classInstance = this;
    
  150.         Scheduler.log(['componentWillMount', findInstance(this)]);
    
  151.       }
    
  152.       componentDidMount() {
    
  153.         Scheduler.log(['componentDidMount', findInstance(this)]);
    
  154.       }
    
  155.       UNSAFE_componentWillUpdate() {
    
  156.         Scheduler.log(['componentWillUpdate', findInstance(this)]);
    
  157.       }
    
  158.       componentDidUpdate() {
    
  159.         Scheduler.log(['componentDidUpdate', findInstance(this)]);
    
  160.       }
    
  161.       componentWillUnmount() {
    
  162.         Scheduler.log(['componentWillUnmount', findInstance(this)]);
    
  163.       }
    
  164.       render() {
    
  165.         Scheduler.log('render');
    
  166.         return this.props.step < 2 ? (
    
  167.           <span ref={ref => (this.span = ref)} />
    
  168.         ) : this.props.step === 2 ? (
    
  169.           <div ref={ref => (this.div = ref)} />
    
  170.         ) : this.props.step === 3 ? null : this.props.step === 4 ? (
    
  171.           <div ref={ref => (this.span = ref)} />
    
  172.         ) : null;
    
  173.       }
    
  174.     }
    
  175. 
    
  176.     function Sibling() {
    
  177.       // Sibling is used to assert that we've rendered past the first component.
    
  178.       Scheduler.log('render sibling');
    
  179.       return <span />;
    
  180.     }
    
  181. 
    
  182.     function Foo(props) {
    
  183.       return [<Component key="a" step={props.step} />, <Sibling key="b" />];
    
  184.     }
    
  185. 
    
  186.     React.startTransition(() => {
    
  187.       ReactNoop.render(<Foo step={0} />);
    
  188.     });
    
  189.     // Flush past Component but don't complete rendering everything yet.
    
  190.     await waitFor([['componentWillMount', null], 'render', 'render sibling']);
    
  191. 
    
  192.     expect(classInstance).toBeDefined();
    
  193.     // The instance has been complete but is still not committed so it should
    
  194.     // not find any host nodes in it.
    
  195.     expect(findInstance(classInstance)).toBe(null);
    
  196. 
    
  197.     await waitForAll([['componentDidMount', span()]]);
    
  198. 
    
  199.     const hostSpan = classInstance.span;
    
  200.     expect(hostSpan).toBeDefined();
    
  201. 
    
  202.     expect(findInstance(classInstance)).toBe(hostSpan);
    
  203. 
    
  204.     // Flush next step which will cause an update but not yet render a new host
    
  205.     // node.
    
  206.     ReactNoop.render(<Foo step={1} />);
    
  207.     await waitForAll([
    
  208.       ['componentWillUpdate', hostSpan],
    
  209.       'render',
    
  210.       'render sibling',
    
  211.       ['componentDidUpdate', hostSpan],
    
  212.     ]);
    
  213. 
    
  214.     expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
    
  215. 
    
  216.     // The next step will render a new host node but won't get committed yet.
    
  217.     // We expect this to mutate the original Fiber.
    
  218.     React.startTransition(() => {
    
  219.       ReactNoop.render(<Foo step={2} />);
    
  220.     });
    
  221.     await waitFor([
    
  222.       ['componentWillUpdate', hostSpan],
    
  223.       'render',
    
  224.       'render sibling',
    
  225.     ]);
    
  226. 
    
  227.     // This should still be the host span.
    
  228.     expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
    
  229. 
    
  230.     // When we finally flush the tree it will get committed.
    
  231.     await waitForAll([['componentDidUpdate', div()]]);
    
  232. 
    
  233.     const hostDiv = classInstance.div;
    
  234.     expect(hostDiv).toBeDefined();
    
  235.     expect(hostSpan).not.toBe(hostDiv);
    
  236. 
    
  237.     // We should now find the new host node.
    
  238.     expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
    
  239. 
    
  240.     // Render to null but don't commit it yet.
    
  241.     React.startTransition(() => {
    
  242.       ReactNoop.render(<Foo step={3} />);
    
  243.     });
    
  244.     await waitFor([
    
  245.       ['componentWillUpdate', hostDiv],
    
  246.       'render',
    
  247.       'render sibling',
    
  248.     ]);
    
  249. 
    
  250.     // This should still be the host div since the deletion is not committed.
    
  251.     expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
    
  252. 
    
  253.     await waitForAll([['componentDidUpdate', null]]);
    
  254. 
    
  255.     // This should still be the host div since the deletion is not committed.
    
  256.     expect(ReactNoop.findInstance(classInstance)).toBe(null);
    
  257. 
    
  258.     // Render a div again
    
  259.     ReactNoop.render(<Foo step={4} />);
    
  260.     await waitForAll([
    
  261.       ['componentWillUpdate', null],
    
  262.       'render',
    
  263.       'render sibling',
    
  264.       ['componentDidUpdate', div()],
    
  265.     ]);
    
  266. 
    
  267.     // Unmount the component.
    
  268.     ReactNoop.render([]);
    
  269.     await waitForAll([['componentWillUnmount', hostDiv]]);
    
  270.   });
    
  271. });