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. const {COMMENT_NODE} = require('react-dom-bindings/src/client/HTMLNodeType');
    
  13. 
    
  14. let React;
    
  15. let ReactDOM;
    
  16. let ReactDOMServer;
    
  17. let ReactTestUtils;
    
  18. 
    
  19. describe('ReactMount', () => {
    
  20.   beforeEach(() => {
    
  21.     jest.resetModules();
    
  22. 
    
  23.     React = require('react');
    
  24.     ReactDOM = require('react-dom');
    
  25.     ReactDOMServer = require('react-dom/server');
    
  26.     ReactTestUtils = require('react-dom/test-utils');
    
  27.   });
    
  28. 
    
  29.   describe('unmountComponentAtNode', () => {
    
  30.     it('throws when given a non-node', () => {
    
  31.       const nodeArray = document.getElementsByTagName('div');
    
  32.       expect(() => {
    
  33.         ReactDOM.unmountComponentAtNode(nodeArray);
    
  34.       }).toThrowError(
    
  35.         'unmountComponentAtNode(...): Target container is not a DOM element.',
    
  36.       );
    
  37.     });
    
  38. 
    
  39.     it('returns false on non-React containers', () => {
    
  40.       const d = document.createElement('div');
    
  41.       d.innerHTML = '<b>hellooo</b>';
    
  42.       expect(ReactDOM.unmountComponentAtNode(d)).toBe(false);
    
  43.       expect(d.textContent).toBe('hellooo');
    
  44.     });
    
  45. 
    
  46.     it('returns true on React containers', () => {
    
  47.       const d = document.createElement('div');
    
  48.       ReactDOM.render(<b>hellooo</b>, d);
    
  49.       expect(d.textContent).toBe('hellooo');
    
  50.       expect(ReactDOM.unmountComponentAtNode(d)).toBe(true);
    
  51.       expect(d.textContent).toBe('');
    
  52.     });
    
  53.   });
    
  54. 
    
  55.   it('warns when given a factory', () => {
    
  56.     class Component extends React.Component {
    
  57.       render() {
    
  58.         return <div />;
    
  59.       }
    
  60.     }
    
  61. 
    
  62.     expect(() => ReactTestUtils.renderIntoDocument(Component)).toErrorDev(
    
  63.       'Functions are not valid as a React child. ' +
    
  64.         'This may happen if you return a Component instead of <Component /> from render. ' +
    
  65.         'Or maybe you meant to call this function rather than return it.',
    
  66.       {withoutStack: true},
    
  67.     );
    
  68.   });
    
  69. 
    
  70.   it('should render different components in same root', () => {
    
  71.     const container = document.createElement('container');
    
  72.     document.body.appendChild(container);
    
  73. 
    
  74.     ReactDOM.render(<div />, container);
    
  75.     expect(container.firstChild.nodeName).toBe('DIV');
    
  76. 
    
  77.     ReactDOM.render(<span />, container);
    
  78.     expect(container.firstChild.nodeName).toBe('SPAN');
    
  79.   });
    
  80. 
    
  81.   it('should unmount and remount if the key changes', () => {
    
  82.     const container = document.createElement('container');
    
  83. 
    
  84.     const mockMount = jest.fn();
    
  85.     const mockUnmount = jest.fn();
    
  86. 
    
  87.     class Component extends React.Component {
    
  88.       componentDidMount = mockMount;
    
  89.       componentWillUnmount = mockUnmount;
    
  90.       render() {
    
  91.         return <span>{this.props.text}</span>;
    
  92.       }
    
  93.     }
    
  94. 
    
  95.     expect(mockMount).toHaveBeenCalledTimes(0);
    
  96.     expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  97. 
    
  98.     ReactDOM.render(<Component text="orange" key="A" />, container);
    
  99.     expect(container.firstChild.innerHTML).toBe('orange');
    
  100.     expect(mockMount).toHaveBeenCalledTimes(1);
    
  101.     expect(mockUnmount).toHaveBeenCalledTimes(0);
    
  102. 
    
  103.     // If we change the key, the component is unmounted and remounted
    
  104.     ReactDOM.render(<Component text="green" key="B" />, container);
    
  105.     expect(container.firstChild.innerHTML).toBe('green');
    
  106.     expect(mockMount).toHaveBeenCalledTimes(2);
    
  107.     expect(mockUnmount).toHaveBeenCalledTimes(1);
    
  108. 
    
  109.     // But if we don't change the key, the component instance is reused
    
  110.     ReactDOM.render(<Component text="blue" key="B" />, container);
    
  111.     expect(container.firstChild.innerHTML).toBe('blue');
    
  112.     expect(mockMount).toHaveBeenCalledTimes(2);
    
  113.     expect(mockUnmount).toHaveBeenCalledTimes(1);
    
  114.   });
    
  115. 
    
  116.   it('should reuse markup if rendering to the same target twice', () => {
    
  117.     const container = document.createElement('container');
    
  118.     const instance1 = ReactDOM.render(<div />, container);
    
  119.     const instance2 = ReactDOM.render(<div />, container);
    
  120. 
    
  121.     expect(instance1 === instance2).toBe(true);
    
  122.   });
    
  123. 
    
  124.   it('does not warn if mounting into left padded rendered markup', () => {
    
  125.     const container = document.createElement('container');
    
  126.     container.innerHTML = ReactDOMServer.renderToString(<div />) + ' ';
    
  127. 
    
  128.     // This should probably ideally warn but we ignore extra markup at the root.
    
  129.     ReactDOM.hydrate(<div />, container);
    
  130.   });
    
  131. 
    
  132.   it('should warn if mounting into right padded rendered markup', () => {
    
  133.     const container = document.createElement('container');
    
  134.     container.innerHTML = ' ' + ReactDOMServer.renderToString(<div />);
    
  135. 
    
  136.     expect(() => ReactDOM.hydrate(<div />, container)).toErrorDev(
    
  137.       'Did not expect server HTML to contain the text node " " in <container>.',
    
  138.     );
    
  139.   });
    
  140. 
    
  141.   it('should not warn if mounting into non-empty node', () => {
    
  142.     const container = document.createElement('container');
    
  143.     container.innerHTML = '<div></div>';
    
  144. 
    
  145.     ReactDOM.render(<div />, container);
    
  146.   });
    
  147. 
    
  148.   it('should warn when mounting into document.body', () => {
    
  149.     const iFrame = document.createElement('iframe');
    
  150.     document.body.appendChild(iFrame);
    
  151. 
    
  152.     if (gate(flags => flags.enableHostSingletons)) {
    
  153.       // HostSingletons make the warning for document.body unecessary
    
  154.       ReactDOM.render(<div />, iFrame.contentDocument.body);
    
  155.     } else {
    
  156.       expect(() =>
    
  157.         ReactDOM.render(<div />, iFrame.contentDocument.body),
    
  158.       ).toErrorDev(
    
  159.         'Rendering components directly into document.body is discouraged',
    
  160.         {withoutStack: true},
    
  161.       );
    
  162.     }
    
  163.   });
    
  164. 
    
  165.   it('should account for escaping on a checksum mismatch', () => {
    
  166.     const div = document.createElement('div');
    
  167.     const markup = ReactDOMServer.renderToString(
    
  168.       <div>This markup contains an nbsp entity: &nbsp; server text</div>,
    
  169.     );
    
  170.     div.innerHTML = markup;
    
  171. 
    
  172.     expect(() =>
    
  173.       ReactDOM.hydrate(
    
  174.         <div>This markup contains an nbsp entity: &nbsp; client text</div>,
    
  175.         div,
    
  176.       ),
    
  177.     ).toErrorDev(
    
  178.       'Server: "This markup contains an nbsp entity:   server text" ' +
    
  179.         'Client: "This markup contains an nbsp entity:   client text"',
    
  180.     );
    
  181.   });
    
  182. 
    
  183.   it('should warn if render removes React-rendered children', () => {
    
  184.     const container = document.createElement('container');
    
  185. 
    
  186.     class Component extends React.Component {
    
  187.       render() {
    
  188.         return (
    
  189.           <div>
    
  190.             <div />
    
  191.           </div>
    
  192.         );
    
  193.       }
    
  194.     }
    
  195. 
    
  196.     ReactDOM.render(<Component />, container);
    
  197. 
    
  198.     // Test that blasting away children throws a warning
    
  199.     const rootNode = container.firstChild;
    
  200. 
    
  201.     expect(() => ReactDOM.render(<span />, rootNode)).toErrorDev(
    
  202.       'Warning: render(...): Replacing React-rendered children with a new ' +
    
  203.         'root component. If you intended to update the children of this node, ' +
    
  204.         'you should instead have the existing children update their state and ' +
    
  205.         'render the new components instead of calling ReactDOM.render.',
    
  206.       {withoutStack: true},
    
  207.     );
    
  208.   });
    
  209. 
    
  210.   it('should warn if the unmounted node was rendered by another copy of React', () => {
    
  211.     jest.resetModules();
    
  212.     const ReactDOMOther = require('react-dom');
    
  213.     const container = document.createElement('div');
    
  214. 
    
  215.     class Component extends React.Component {
    
  216.       render() {
    
  217.         return (
    
  218.           <div>
    
  219.             <div />
    
  220.           </div>
    
  221.         );
    
  222.       }
    
  223.     }
    
  224. 
    
  225.     ReactDOM.render(<Component />, container);
    
  226.     // Make sure ReactDOM and ReactDOMOther are different copies
    
  227.     expect(ReactDOM).not.toEqual(ReactDOMOther);
    
  228. 
    
  229.     expect(() => ReactDOMOther.unmountComponentAtNode(container)).toErrorDev(
    
  230.       "Warning: unmountComponentAtNode(): The node you're attempting to unmount " +
    
  231.         'was rendered by another copy of React.',
    
  232.       {withoutStack: true},
    
  233.     );
    
  234. 
    
  235.     // Don't throw a warning if the correct React copy unmounts the node
    
  236.     ReactDOM.unmountComponentAtNode(container);
    
  237.   });
    
  238. 
    
  239.   it('passes the correct callback context', () => {
    
  240.     const container = document.createElement('div');
    
  241.     let calls = 0;
    
  242. 
    
  243.     ReactDOM.render(<div />, container, function () {
    
  244.       expect(this.nodeName).toBe('DIV');
    
  245.       calls++;
    
  246.     });
    
  247. 
    
  248.     // Update, no type change
    
  249.     ReactDOM.render(<div />, container, function () {
    
  250.       expect(this.nodeName).toBe('DIV');
    
  251.       calls++;
    
  252.     });
    
  253. 
    
  254.     // Update, type change
    
  255.     ReactDOM.render(<span />, container, function () {
    
  256.       expect(this.nodeName).toBe('SPAN');
    
  257.       calls++;
    
  258.     });
    
  259. 
    
  260.     // Batched update, no type change
    
  261.     ReactDOM.unstable_batchedUpdates(function () {
    
  262.       ReactDOM.render(<span />, container, function () {
    
  263.         expect(this.nodeName).toBe('SPAN');
    
  264.         calls++;
    
  265.       });
    
  266.     });
    
  267. 
    
  268.     // Batched update, type change
    
  269.     ReactDOM.unstable_batchedUpdates(function () {
    
  270.       ReactDOM.render(<article />, container, function () {
    
  271.         expect(this.nodeName).toBe('ARTICLE');
    
  272.         calls++;
    
  273.       });
    
  274.     });
    
  275. 
    
  276.     expect(calls).toBe(5);
    
  277.   });
    
  278. 
    
  279.   it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
    
  280.     const container1 = document.createElement('div');
    
  281.     const container2 = document.createElement('div');
    
  282. 
    
  283.     class Foo extends React.Component {
    
  284.       state = {active: false};
    
  285.       componentDidMount() {
    
  286.         this.setState({active: true});
    
  287.       }
    
  288.       render() {
    
  289.         return (
    
  290.           <div>{this.props.children + (this.state.active ? '!' : '')}</div>
    
  291.         );
    
  292.       }
    
  293.     }
    
  294. 
    
  295.     ReactDOM.render(<div>1</div>, container1);
    
  296. 
    
  297.     ReactDOM.unstable_batchedUpdates(() => {
    
  298.       // Update. Does not flush yet.
    
  299.       ReactDOM.render(<div>2</div>, container1);
    
  300.       expect(container1.textContent).toEqual('1');
    
  301. 
    
  302.       // Initial mount on another root. Should flush immediately.
    
  303.       ReactDOM.render(<Foo>a</Foo>, container2);
    
  304.       // The earlier update also flushed, since flushSync flushes all pending
    
  305.       // sync work across all roots.
    
  306.       expect(container1.textContent).toEqual('2');
    
  307.       // Layout updates are also flushed synchronously
    
  308.       expect(container2.textContent).toEqual('a!');
    
  309.     });
    
  310.     expect(container1.textContent).toEqual('2');
    
  311.     expect(container2.textContent).toEqual('a!');
    
  312.   });
    
  313. 
    
  314.   describe('mount point is a comment node', () => {
    
  315.     let containerDiv;
    
  316.     let mountPoint;
    
  317. 
    
  318.     beforeEach(() => {
    
  319.       containerDiv = document.createElement('div');
    
  320.       containerDiv.innerHTML = 'A<!-- react-mount-point-unstable -->B';
    
  321.       mountPoint = containerDiv.childNodes[1];
    
  322.       expect(mountPoint.nodeType).toBe(COMMENT_NODE);
    
  323.     });
    
  324. 
    
  325.     it('renders at a comment node', () => {
    
  326.       function Char(props) {
    
  327.         return props.children;
    
  328.       }
    
  329.       function list(chars) {
    
  330.         return chars.split('').map(c => <Char key={c}>{c}</Char>);
    
  331.       }
    
  332. 
    
  333.       ReactDOM.render(list('aeiou'), mountPoint);
    
  334.       expect(containerDiv.innerHTML).toBe(
    
  335.         'Aaeiou<!-- react-mount-point-unstable -->B',
    
  336.       );
    
  337. 
    
  338.       ReactDOM.render(list('yea'), mountPoint);
    
  339.       expect(containerDiv.innerHTML).toBe(
    
  340.         'Ayea<!-- react-mount-point-unstable -->B',
    
  341.       );
    
  342. 
    
  343.       ReactDOM.render(list(''), mountPoint);
    
  344.       expect(containerDiv.innerHTML).toBe(
    
  345.         'A<!-- react-mount-point-unstable -->B',
    
  346.       );
    
  347.     });
    
  348.   });
    
  349. });