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. describe('ReactMultiChild', () => {
    
  13.   let React;
    
  14.   let ReactDOM;
    
  15. 
    
  16.   beforeEach(() => {
    
  17.     jest.resetModules();
    
  18.     React = require('react');
    
  19.     ReactDOM = require('react-dom');
    
  20.   });
    
  21. 
    
  22.   describe('reconciliation', () => {
    
  23.     it('should update children when possible', () => {
    
  24.       const container = document.createElement('div');
    
  25. 
    
  26.       const mockMount = jest.fn();
    
  27.       const mockUpdate = jest.fn();
    
  28.       const mockUnmount = jest.fn();
    
  29. 
    
  30.       class MockComponent extends React.Component {
    
  31.         componentDidMount = mockMount;
    
  32.         componentDidUpdate = mockUpdate;
    
  33.         componentWillUnmount = mockUnmount;
    
  34.         render() {
    
  35.           return <span />;
    
  36.         }
    
  37.       }
    
  38. 
    
  39.       expect(mockMount).toHaveBeenCalledTimes(0);
    
  40.       expect(mockUpdate).toHaveBeenCalledTimes(0);
    
  41.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  42. 
    
  43.       ReactDOM.render(
    
  44.         <div>
    
  45.           <MockComponent />
    
  46.         </div>,
    
  47.         container,
    
  48.       );
    
  49. 
    
  50.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  51.       expect(mockUpdate).toHaveBeenCalledTimes(0);
    
  52.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  53. 
    
  54.       ReactDOM.render(
    
  55.         <div>
    
  56.           <MockComponent />
    
  57.         </div>,
    
  58.         container,
    
  59.       );
    
  60. 
    
  61.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  62.       expect(mockUpdate).toHaveBeenCalledTimes(1);
    
  63.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  64.     });
    
  65. 
    
  66.     it('should replace children with different constructors', () => {
    
  67.       const container = document.createElement('div');
    
  68. 
    
  69.       const mockMount = jest.fn();
    
  70.       const mockUnmount = jest.fn();
    
  71. 
    
  72.       class MockComponent extends React.Component {
    
  73.         componentDidMount = mockMount;
    
  74.         componentWillUnmount = mockUnmount;
    
  75.         render() {
    
  76.           return <span />;
    
  77.         }
    
  78.       }
    
  79. 
    
  80.       expect(mockMount).toHaveBeenCalledTimes(0);
    
  81.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  82. 
    
  83.       ReactDOM.render(
    
  84.         <div>
    
  85.           <MockComponent />
    
  86.         </div>,
    
  87.         container,
    
  88.       );
    
  89. 
    
  90.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  91.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  92. 
    
  93.       ReactDOM.render(
    
  94.         <div>
    
  95.           <span />
    
  96.         </div>,
    
  97.         container,
    
  98.       );
    
  99. 
    
  100.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  101.       expect(mockUnmount).toHaveBeenCalledTimes(1);
    
  102.     });
    
  103. 
    
  104.     it('should NOT replace children with different owners', () => {
    
  105.       const container = document.createElement('div');
    
  106. 
    
  107.       const mockMount = jest.fn();
    
  108.       const mockUnmount = jest.fn();
    
  109. 
    
  110.       class MockComponent extends React.Component {
    
  111.         componentDidMount = mockMount;
    
  112.         componentWillUnmount = mockUnmount;
    
  113.         render() {
    
  114.           return <span />;
    
  115.         }
    
  116.       }
    
  117. 
    
  118.       class WrapperComponent extends React.Component {
    
  119.         render() {
    
  120.           return this.props.children || <MockComponent />;
    
  121.         }
    
  122.       }
    
  123. 
    
  124.       expect(mockMount).toHaveBeenCalledTimes(0);
    
  125.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  126. 
    
  127.       ReactDOM.render(<WrapperComponent />, container);
    
  128. 
    
  129.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  130.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  131. 
    
  132.       ReactDOM.render(
    
  133.         <WrapperComponent>
    
  134.           <MockComponent />
    
  135.         </WrapperComponent>,
    
  136.         container,
    
  137.       );
    
  138. 
    
  139.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  140.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  141.     });
    
  142. 
    
  143.     it('should replace children with different keys', () => {
    
  144.       const container = document.createElement('div');
    
  145. 
    
  146.       const mockMount = jest.fn();
    
  147.       const mockUnmount = jest.fn();
    
  148. 
    
  149.       class MockComponent extends React.Component {
    
  150.         componentDidMount = mockMount;
    
  151.         componentWillUnmount = mockUnmount;
    
  152.         render() {
    
  153.           return <span />;
    
  154.         }
    
  155.       }
    
  156. 
    
  157.       expect(mockMount).toHaveBeenCalledTimes(0);
    
  158.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  159. 
    
  160.       ReactDOM.render(
    
  161.         <div>
    
  162.           <MockComponent key="A" />
    
  163.         </div>,
    
  164.         container,
    
  165.       );
    
  166. 
    
  167.       expect(mockMount).toHaveBeenCalledTimes(1);
    
  168.       expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  169. 
    
  170.       ReactDOM.render(
    
  171.         <div>
    
  172.           <MockComponent key="B" />
    
  173.         </div>,
    
  174.         container,
    
  175.       );
    
  176. 
    
  177.       expect(mockMount).toHaveBeenCalledTimes(2);
    
  178.       expect(mockUnmount).toHaveBeenCalledTimes(1);
    
  179.     });
    
  180. 
    
  181.     it('should warn for duplicated array keys with component stack info', () => {
    
  182.       const container = document.createElement('div');
    
  183. 
    
  184.       class WrapperComponent extends React.Component {
    
  185.         render() {
    
  186.           return <div>{this.props.children}</div>;
    
  187.         }
    
  188.       }
    
  189. 
    
  190.       class Parent extends React.Component {
    
  191.         render() {
    
  192.           return (
    
  193.             <div>
    
  194.               <WrapperComponent>{this.props.children}</WrapperComponent>
    
  195.             </div>
    
  196.           );
    
  197.         }
    
  198.       }
    
  199. 
    
  200.       ReactDOM.render(<Parent>{[<div key="1" />]}</Parent>, container);
    
  201. 
    
  202.       expect(() =>
    
  203.         ReactDOM.render(
    
  204.           <Parent>{[<div key="1" />, <div key="1" />]}</Parent>,
    
  205.           container,
    
  206.         ),
    
  207.       ).toErrorDev(
    
  208.         'Encountered two children with the same key, `1`. ' +
    
  209.           'Keys should be unique so that components maintain their identity ' +
    
  210.           'across updates. Non-unique keys may cause children to be ' +
    
  211.           'duplicated and/or omitted — the behavior is unsupported and ' +
    
  212.           'could change in a future version.\n' +
    
  213.           '    in div (at **)\n' +
    
  214.           '    in WrapperComponent (at **)\n' +
    
  215.           '    in div (at **)\n' +
    
  216.           '    in Parent (at **)',
    
  217.       );
    
  218.     });
    
  219. 
    
  220.     it('should warn for duplicated iterable keys with component stack info', () => {
    
  221.       const container = document.createElement('div');
    
  222. 
    
  223.       class WrapperComponent extends React.Component {
    
  224.         render() {
    
  225.           return <div>{this.props.children}</div>;
    
  226.         }
    
  227.       }
    
  228. 
    
  229.       class Parent extends React.Component {
    
  230.         render() {
    
  231.           return (
    
  232.             <div>
    
  233.               <WrapperComponent>{this.props.children}</WrapperComponent>
    
  234.             </div>
    
  235.           );
    
  236.         }
    
  237.       }
    
  238. 
    
  239.       function createIterable(array) {
    
  240.         return {
    
  241.           '@@iterator': function () {
    
  242.             let i = 0;
    
  243.             return {
    
  244.               next() {
    
  245.                 const next = {
    
  246.                   value: i < array.length ? array[i] : undefined,
    
  247.                   done: i === array.length,
    
  248.                 };
    
  249.                 i++;
    
  250.                 return next;
    
  251.               },
    
  252.             };
    
  253.           },
    
  254.         };
    
  255.       }
    
  256. 
    
  257.       ReactDOM.render(
    
  258.         <Parent>{createIterable([<div key="1" />])}</Parent>,
    
  259.         container,
    
  260.       );
    
  261. 
    
  262.       expect(() =>
    
  263.         ReactDOM.render(
    
  264.           <Parent>{createIterable([<div key="1" />, <div key="1" />])}</Parent>,
    
  265.           container,
    
  266.         ),
    
  267.       ).toErrorDev(
    
  268.         'Encountered two children with the same key, `1`. ' +
    
  269.           'Keys should be unique so that components maintain their identity ' +
    
  270.           'across updates. Non-unique keys may cause children to be ' +
    
  271.           'duplicated and/or omitted — the behavior is unsupported and ' +
    
  272.           'could change in a future version.\n' +
    
  273.           '    in div (at **)\n' +
    
  274.           '    in WrapperComponent (at **)\n' +
    
  275.           '    in div (at **)\n' +
    
  276.           '    in Parent (at **)',
    
  277.       );
    
  278.     });
    
  279.   });
    
  280. 
    
  281.   it('should warn for using maps as children with owner info', () => {
    
  282.     class Parent extends React.Component {
    
  283.       render() {
    
  284.         return (
    
  285.           <div>
    
  286.             {
    
  287.               new Map([
    
  288.                 ['foo', 0],
    
  289.                 ['bar', 1],
    
  290.               ])
    
  291.             }
    
  292.           </div>
    
  293.         );
    
  294.       }
    
  295.     }
    
  296.     const container = document.createElement('div');
    
  297.     expect(() => ReactDOM.render(<Parent />, container)).toErrorDev(
    
  298.       'Using Maps as children is not supported. ' +
    
  299.         'Use an array of keyed ReactElements instead.\n' +
    
  300.         '    in div (at **)\n' +
    
  301.         '    in Parent (at **)',
    
  302.     );
    
  303.   });
    
  304. 
    
  305.   it('should warn for using generators as children', () => {
    
  306.     function* Foo() {
    
  307.       yield <h1 key="1">Hello</h1>;
    
  308.       yield <h1 key="2">World</h1>;
    
  309.     }
    
  310. 
    
  311.     const div = document.createElement('div');
    
  312.     expect(() => {
    
  313.       ReactDOM.render(<Foo />, div);
    
  314.     }).toErrorDev(
    
  315.       'Using Generators as children is unsupported and will likely yield ' +
    
  316.         'unexpected results because enumerating a generator mutates it. You may ' +
    
  317.         'convert it to an array with `Array.from()` or the `[...spread]` operator ' +
    
  318.         'before rendering. Keep in mind you might need to polyfill these features for older browsers.\n' +
    
  319.         '    in Foo (at **)',
    
  320.     );
    
  321. 
    
  322.     // Test de-duplication
    
  323.     ReactDOM.render(<Foo />, div);
    
  324.   });
    
  325. 
    
  326.   it('should not warn for using generators in legacy iterables', () => {
    
  327.     const fooIterable = {
    
  328.       '@@iterator': function* () {
    
  329.         yield <h1 key="1">Hello</h1>;
    
  330.         yield <h1 key="2">World</h1>;
    
  331.       },
    
  332.     };
    
  333. 
    
  334.     function Foo() {
    
  335.       return fooIterable;
    
  336.     }
    
  337. 
    
  338.     const div = document.createElement('div');
    
  339.     ReactDOM.render(<Foo />, div);
    
  340.     expect(div.textContent).toBe('HelloWorld');
    
  341. 
    
  342.     ReactDOM.render(<Foo />, div);
    
  343.     expect(div.textContent).toBe('HelloWorld');
    
  344.   });
    
  345. 
    
  346.   it('should not warn for using generators in modern iterables', () => {
    
  347.     const fooIterable = {
    
  348.       [Symbol.iterator]: function* () {
    
  349.         yield <h1 key="1">Hello</h1>;
    
  350.         yield <h1 key="2">World</h1>;
    
  351.       },
    
  352.     };
    
  353. 
    
  354.     function Foo() {
    
  355.       return fooIterable;
    
  356.     }
    
  357. 
    
  358.     const div = document.createElement('div');
    
  359.     ReactDOM.render(<Foo />, div);
    
  360.     expect(div.textContent).toBe('HelloWorld');
    
  361. 
    
  362.     ReactDOM.render(<Foo />, div);
    
  363.     expect(div.textContent).toBe('HelloWorld');
    
  364.   });
    
  365. 
    
  366.   it('should reorder bailed-out children', () => {
    
  367.     class LetterInner extends React.Component {
    
  368.       render() {
    
  369.         return <div>{this.props.char}</div>;
    
  370.       }
    
  371.     }
    
  372. 
    
  373.     class Letter extends React.Component {
    
  374.       render() {
    
  375.         return <LetterInner char={this.props.char} />;
    
  376.       }
    
  377.       shouldComponentUpdate() {
    
  378.         return false;
    
  379.       }
    
  380.     }
    
  381. 
    
  382.     class Letters extends React.Component {
    
  383.       render() {
    
  384.         const letters = this.props.letters.split('');
    
  385.         return (
    
  386.           <div>
    
  387.             {letters.map(c => (
    
  388.               <Letter key={c} char={c} />
    
  389.             ))}
    
  390.           </div>
    
  391.         );
    
  392.       }
    
  393.     }
    
  394. 
    
  395.     const container = document.createElement('div');
    
  396. 
    
  397.     // Two random strings -- some additions, some removals, some moves
    
  398.     ReactDOM.render(<Letters letters="XKwHomsNjIkBcQWFbiZU" />, container);
    
  399.     expect(container.textContent).toBe('XKwHomsNjIkBcQWFbiZU');
    
  400.     ReactDOM.render(<Letters letters="EHCjpdTUuiybDvhRJwZt" />, container);
    
  401.     expect(container.textContent).toBe('EHCjpdTUuiybDvhRJwZt');
    
  402.   });
    
  403. 
    
  404.   it('prepares new children before unmounting old', () => {
    
  405.     const log = [];
    
  406. 
    
  407.     class Spy extends React.Component {
    
  408.       UNSAFE_componentWillMount() {
    
  409.         log.push(this.props.name + ' componentWillMount');
    
  410.       }
    
  411.       render() {
    
  412.         log.push(this.props.name + ' render');
    
  413.         return <div />;
    
  414.       }
    
  415.       componentDidMount() {
    
  416.         log.push(this.props.name + ' componentDidMount');
    
  417.       }
    
  418.       componentWillUnmount() {
    
  419.         log.push(this.props.name + ' componentWillUnmount');
    
  420.       }
    
  421.     }
    
  422. 
    
  423.     // These are reference-unequal so they will be swapped even if they have
    
  424.     // matching keys
    
  425.     const SpyA = props => <Spy {...props} />;
    
  426.     const SpyB = props => <Spy {...props} />;
    
  427. 
    
  428.     const container = document.createElement('div');
    
  429.     ReactDOM.render(
    
  430.       <div>
    
  431.         <SpyA key="one" name="oneA" />
    
  432.         <SpyA key="two" name="twoA" />
    
  433.       </div>,
    
  434.       container,
    
  435.     );
    
  436.     ReactDOM.render(
    
  437.       <div>
    
  438.         <SpyB key="one" name="oneB" />
    
  439.         <SpyB key="two" name="twoB" />
    
  440.       </div>,
    
  441.       container,
    
  442.     );
    
  443. 
    
  444.     expect(log).toEqual([
    
  445.       'oneA componentWillMount',
    
  446.       'oneA render',
    
  447.       'twoA componentWillMount',
    
  448.       'twoA render',
    
  449.       'oneA componentDidMount',
    
  450.       'twoA componentDidMount',
    
  451. 
    
  452.       'oneB componentWillMount',
    
  453.       'oneB render',
    
  454.       'twoB componentWillMount',
    
  455.       'twoB render',
    
  456.       'oneA componentWillUnmount',
    
  457.       'twoA componentWillUnmount',
    
  458. 
    
  459.       'oneB componentDidMount',
    
  460.       'twoB componentDidMount',
    
  461.     ]);
    
  462.   });
    
  463. });