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. let React;
    
  13. let ReactDOM;
    
  14. let ReactDOMServer;
    
  15. let ReactTestUtils;
    
  16. 
    
  17. describe('ReactDOM', () => {
    
  18.   beforeEach(() => {
    
  19.     jest.resetModules();
    
  20.     React = require('react');
    
  21.     ReactDOM = require('react-dom');
    
  22.     ReactDOMServer = require('react-dom/server');
    
  23.     ReactTestUtils = require('react-dom/test-utils');
    
  24.   });
    
  25. 
    
  26.   it('should bubble onSubmit', function () {
    
  27.     const container = document.createElement('div');
    
  28. 
    
  29.     let count = 0;
    
  30.     let buttonRef;
    
  31. 
    
  32.     function Parent() {
    
  33.       return (
    
  34.         <div
    
  35.           onSubmit={event => {
    
  36.             event.preventDefault();
    
  37.             count++;
    
  38.           }}>
    
  39.           <Child />
    
  40.         </div>
    
  41.       );
    
  42.     }
    
  43. 
    
  44.     function Child() {
    
  45.       return (
    
  46.         <form>
    
  47.           <input type="submit" ref={button => (buttonRef = button)} />
    
  48.         </form>
    
  49.       );
    
  50.     }
    
  51. 
    
  52.     document.body.appendChild(container);
    
  53.     try {
    
  54.       ReactDOM.render(<Parent />, container);
    
  55.       buttonRef.click();
    
  56.       expect(count).toBe(1);
    
  57.     } finally {
    
  58.       document.body.removeChild(container);
    
  59.     }
    
  60.   });
    
  61. 
    
  62.   it('allows a DOM element to be used with a string', () => {
    
  63.     const element = React.createElement('div', {className: 'foo'});
    
  64.     const node = ReactTestUtils.renderIntoDocument(element);
    
  65.     expect(node.tagName).toBe('DIV');
    
  66.   });
    
  67. 
    
  68.   it('should allow children to be passed as an argument', () => {
    
  69.     const argNode = ReactTestUtils.renderIntoDocument(
    
  70.       React.createElement('div', null, 'child'),
    
  71.     );
    
  72.     expect(argNode.innerHTML).toBe('child');
    
  73.   });
    
  74. 
    
  75.   it('should overwrite props.children with children argument', () => {
    
  76.     const conflictNode = ReactTestUtils.renderIntoDocument(
    
  77.       React.createElement('div', {children: 'fakechild'}, 'child'),
    
  78.     );
    
  79.     expect(conflictNode.innerHTML).toBe('child');
    
  80.   });
    
  81. 
    
  82.   /**
    
  83.    * We need to make sure that updates occur to the actual node that's in the
    
  84.    * DOM, instead of a stale cache.
    
  85.    */
    
  86.   it('should purge the DOM cache when removing nodes', () => {
    
  87.     let myDiv = ReactTestUtils.renderIntoDocument(
    
  88.       <div>
    
  89.         <div key="theDog" className="dog" />,
    
  90.         <div key="theBird" className="bird" />
    
  91.       </div>,
    
  92.     );
    
  93.     // Warm the cache with theDog
    
  94.     myDiv = ReactTestUtils.renderIntoDocument(
    
  95.       <div>
    
  96.         <div key="theDog" className="dogbeforedelete" />,
    
  97.         <div key="theBird" className="bird" />,
    
  98.       </div>,
    
  99.     );
    
  100.     // Remove theDog - this should purge the cache
    
  101.     myDiv = ReactTestUtils.renderIntoDocument(
    
  102.       <div>
    
  103.         <div key="theBird" className="bird" />,
    
  104.       </div>,
    
  105.     );
    
  106.     // Now, put theDog back. It's now a different DOM node.
    
  107.     myDiv = ReactTestUtils.renderIntoDocument(
    
  108.       <div>
    
  109.         <div key="theDog" className="dog" />,
    
  110.         <div key="theBird" className="bird" />,
    
  111.       </div>,
    
  112.     );
    
  113.     // Change the className of theDog. It will use the same element
    
  114.     myDiv = ReactTestUtils.renderIntoDocument(
    
  115.       <div>
    
  116.         <div key="theDog" className="bigdog" />,
    
  117.         <div key="theBird" className="bird" />,
    
  118.       </div>,
    
  119.     );
    
  120.     const dog = myDiv.childNodes[0];
    
  121.     expect(dog.className).toBe('bigdog');
    
  122.   });
    
  123. 
    
  124.   it('throws in render() if the mount callback is not a function', () => {
    
  125.     function Foo() {
    
  126.       this.a = 1;
    
  127.       this.b = 2;
    
  128.     }
    
  129. 
    
  130.     class A extends React.Component {
    
  131.       state = {};
    
  132. 
    
  133.       render() {
    
  134.         return <div />;
    
  135.       }
    
  136.     }
    
  137. 
    
  138.     const myDiv = document.createElement('div');
    
  139.     expect(() => {
    
  140.       expect(() => {
    
  141.         ReactDOM.render(<A />, myDiv, 'no');
    
  142.       }).toErrorDev(
    
  143.         'render(...): Expected the last optional `callback` argument to be ' +
    
  144.           'a function. Instead received: no.',
    
  145.       );
    
  146.     }).toThrowError(
    
  147.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  148.         'received: no',
    
  149.     );
    
  150. 
    
  151.     expect(() => {
    
  152.       expect(() => {
    
  153.         ReactDOM.render(<A />, myDiv, {foo: 'bar'});
    
  154.       }).toErrorDev(
    
  155.         'render(...): Expected the last optional `callback` argument to be ' +
    
  156.           'a function. Instead received: [object Object].',
    
  157.       );
    
  158.     }).toThrowError(
    
  159.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  160.         'received: [object Object]',
    
  161.     );
    
  162. 
    
  163.     expect(() => {
    
  164.       expect(() => {
    
  165.         ReactDOM.render(<A />, myDiv, new Foo());
    
  166.       }).toErrorDev(
    
  167.         'render(...): Expected the last optional `callback` argument to be ' +
    
  168.           'a function. Instead received: [object Object].',
    
  169.       );
    
  170.     }).toThrowError(
    
  171.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  172.         'received: [object Object]',
    
  173.     );
    
  174.   });
    
  175. 
    
  176.   it('throws in render() if the update callback is not a function', () => {
    
  177.     function Foo() {
    
  178.       this.a = 1;
    
  179.       this.b = 2;
    
  180.     }
    
  181. 
    
  182.     class A extends React.Component {
    
  183.       state = {};
    
  184. 
    
  185.       render() {
    
  186.         return <div />;
    
  187.       }
    
  188.     }
    
  189. 
    
  190.     const myDiv = document.createElement('div');
    
  191.     ReactDOM.render(<A />, myDiv);
    
  192.     expect(() => {
    
  193.       expect(() => {
    
  194.         ReactDOM.render(<A />, myDiv, 'no');
    
  195.       }).toErrorDev(
    
  196.         'render(...): Expected the last optional `callback` argument to be ' +
    
  197.           'a function. Instead received: no.',
    
  198.       );
    
  199.     }).toThrowError(
    
  200.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  201.         'received: no',
    
  202.     );
    
  203. 
    
  204.     ReactDOM.render(<A />, myDiv); // Re-mount
    
  205.     expect(() => {
    
  206.       expect(() => {
    
  207.         ReactDOM.render(<A />, myDiv, {foo: 'bar'});
    
  208.       }).toErrorDev(
    
  209.         'render(...): Expected the last optional `callback` argument to be ' +
    
  210.           'a function. Instead received: [object Object].',
    
  211.       );
    
  212.     }).toThrowError(
    
  213.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  214.         'received: [object Object]',
    
  215.     );
    
  216. 
    
  217.     ReactDOM.render(<A />, myDiv); // Re-mount
    
  218.     expect(() => {
    
  219.       expect(() => {
    
  220.         ReactDOM.render(<A />, myDiv, new Foo());
    
  221.       }).toErrorDev(
    
  222.         'render(...): Expected the last optional `callback` argument to be ' +
    
  223.           'a function. Instead received: [object Object].',
    
  224.       );
    
  225.     }).toThrowError(
    
  226.       'Invalid argument passed as callback. Expected a function. Instead ' +
    
  227.         'received: [object Object]',
    
  228.     );
    
  229.   });
    
  230. 
    
  231.   it('preserves focus', () => {
    
  232.     let input;
    
  233.     let input2;
    
  234.     class A extends React.Component {
    
  235.       render() {
    
  236.         return (
    
  237.           <div>
    
  238.             <input id="one" ref={r => (input = input || r)} />
    
  239.             {this.props.showTwo && (
    
  240.               <input id="two" ref={r => (input2 = input2 || r)} />
    
  241.             )}
    
  242.           </div>
    
  243.         );
    
  244.       }
    
  245. 
    
  246.       componentDidUpdate() {
    
  247.         // Focus should have been restored to the original input
    
  248.         expect(document.activeElement.id).toBe('one');
    
  249.         input2.focus();
    
  250.         expect(document.activeElement.id).toBe('two');
    
  251.         log.push('input2 focused');
    
  252.       }
    
  253.     }
    
  254. 
    
  255.     const log = [];
    
  256.     const container = document.createElement('div');
    
  257.     document.body.appendChild(container);
    
  258.     try {
    
  259.       ReactDOM.render(<A showTwo={false} />, container);
    
  260.       input.focus();
    
  261. 
    
  262.       // When the second input is added, let's simulate losing focus, which is
    
  263.       // something that could happen when manipulating DOM nodes (but is hard to
    
  264.       // deterministically force without relying intensely on React DOM
    
  265.       // implementation details)
    
  266.       const div = container.firstChild;
    
  267.       ['appendChild', 'insertBefore'].forEach(name => {
    
  268.         const mutator = div[name];
    
  269.         div[name] = function () {
    
  270.           if (input) {
    
  271.             input.blur();
    
  272.             expect(document.activeElement.tagName).toBe('BODY');
    
  273.             log.push('input2 inserted');
    
  274.           }
    
  275.           return mutator.apply(this, arguments);
    
  276.         };
    
  277.       });
    
  278. 
    
  279.       expect(document.activeElement.id).toBe('one');
    
  280.       ReactDOM.render(<A showTwo={true} />, container);
    
  281.       // input2 gets added, which causes input to get blurred. Then
    
  282.       // componentDidUpdate focuses input2 and that should make it down to here,
    
  283.       // not get overwritten by focus restoration.
    
  284.       expect(document.activeElement.id).toBe('two');
    
  285.       expect(log).toEqual(['input2 inserted', 'input2 focused']);
    
  286.     } finally {
    
  287.       document.body.removeChild(container);
    
  288.     }
    
  289.   });
    
  290. 
    
  291.   it('calls focus() on autoFocus elements after they have been mounted to the DOM', () => {
    
  292.     const originalFocus = HTMLElement.prototype.focus;
    
  293. 
    
  294.     try {
    
  295.       let focusedElement;
    
  296.       let inputFocusedAfterMount = false;
    
  297. 
    
  298.       // This test needs to determine that focus is called after mount.
    
  299.       // Can't check document.activeElement because PhantomJS is too permissive;
    
  300.       // It doesn't require element to be in the DOM to be focused.
    
  301.       HTMLElement.prototype.focus = function () {
    
  302.         focusedElement = this;
    
  303.         inputFocusedAfterMount = !!this.parentNode;
    
  304.       };
    
  305. 
    
  306.       const container = document.createElement('div');
    
  307.       document.body.appendChild(container);
    
  308.       ReactDOM.render(
    
  309.         <div>
    
  310.           <h1>Auto-focus Test</h1>
    
  311.           <input autoFocus={true} />
    
  312.           <p>The above input should be focused after mount.</p>
    
  313.         </div>,
    
  314.         container,
    
  315.       );
    
  316. 
    
  317.       expect(inputFocusedAfterMount).toBe(true);
    
  318.       expect(focusedElement.tagName).toBe('INPUT');
    
  319.     } finally {
    
  320.       HTMLElement.prototype.focus = originalFocus;
    
  321.     }
    
  322.   });
    
  323. 
    
  324.   it("shouldn't fire duplicate event handler while handling other nested dispatch", () => {
    
  325.     const actual = [];
    
  326. 
    
  327.     class Wrapper extends React.Component {
    
  328.       componentDidMount() {
    
  329.         this.ref1.click();
    
  330.       }
    
  331. 
    
  332.       render() {
    
  333.         return (
    
  334.           <div>
    
  335.             <div
    
  336.               onClick={() => {
    
  337.                 actual.push('1st node clicked');
    
  338.                 this.ref2.click();
    
  339.               }}
    
  340.               ref={ref => (this.ref1 = ref)}
    
  341.             />
    
  342.             <div
    
  343.               onClick={ref => {
    
  344.                 actual.push("2nd node clicked imperatively from 1st's handler");
    
  345.               }}
    
  346.               ref={ref => (this.ref2 = ref)}
    
  347.             />
    
  348.           </div>
    
  349.         );
    
  350.       }
    
  351.     }
    
  352. 
    
  353.     const container = document.createElement('div');
    
  354.     document.body.appendChild(container);
    
  355.     try {
    
  356.       ReactDOM.render(<Wrapper />, container);
    
  357. 
    
  358.       const expected = [
    
  359.         '1st node clicked',
    
  360.         "2nd node clicked imperatively from 1st's handler",
    
  361.       ];
    
  362. 
    
  363.       expect(actual).toEqual(expected);
    
  364.     } finally {
    
  365.       document.body.removeChild(container);
    
  366.     }
    
  367.   });
    
  368. 
    
  369.   it('should not crash with devtools installed', () => {
    
  370.     try {
    
  371.       global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
    
  372.         inject: function () {},
    
  373.         onCommitFiberRoot: function () {},
    
  374.         onCommitFiberUnmount: function () {},
    
  375.         supportsFiber: true,
    
  376.       };
    
  377.       jest.resetModules();
    
  378.       React = require('react');
    
  379.       ReactDOM = require('react-dom');
    
  380.       class Component extends React.Component {
    
  381.         render() {
    
  382.           return <div />;
    
  383.         }
    
  384.       }
    
  385.       ReactDOM.render(<Component />, document.createElement('container'));
    
  386.     } finally {
    
  387.       delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    
  388.     }
    
  389.   });
    
  390. 
    
  391.   it('should not crash calling findDOMNode inside a function component', () => {
    
  392.     const container = document.createElement('div');
    
  393. 
    
  394.     class Component extends React.Component {
    
  395.       render() {
    
  396.         return <div />;
    
  397.       }
    
  398.     }
    
  399. 
    
  400.     const instance = ReactTestUtils.renderIntoDocument(<Component />);
    
  401.     const App = () => {
    
  402.       ReactDOM.findDOMNode(instance);
    
  403.       return <div />;
    
  404.     };
    
  405. 
    
  406.     if (__DEV__) {
    
  407.       ReactDOM.render(<App />, container);
    
  408.     }
    
  409.   });
    
  410. 
    
  411.   it('reports stacks with re-entrant renderToString() calls on the client', () => {
    
  412.     function Child2(props) {
    
  413.       return <span ariaTypo3="no">{props.children}</span>;
    
  414.     }
    
  415. 
    
  416.     function App2() {
    
  417.       return (
    
  418.         <Child2>
    
  419.           {ReactDOMServer.renderToString(<blink ariaTypo2="no" />)}
    
  420.         </Child2>
    
  421.       );
    
  422.     }
    
  423. 
    
  424.     function Child() {
    
  425.       return (
    
  426.         <span ariaTypo4="no">{ReactDOMServer.renderToString(<App2 />)}</span>
    
  427.       );
    
  428.     }
    
  429. 
    
  430.     function ServerEntry() {
    
  431.       return ReactDOMServer.renderToString(<Child />);
    
  432.     }
    
  433. 
    
  434.     function App() {
    
  435.       return (
    
  436.         <div>
    
  437.           <span ariaTypo="no" />
    
  438.           <ServerEntry />
    
  439.           <font ariaTypo5="no" />
    
  440.         </div>
    
  441.       );
    
  442.     }
    
  443. 
    
  444.     const container = document.createElement('div');
    
  445.     expect(() => ReactDOM.render(<App />, container)).toErrorDev([
    
  446.       // ReactDOM(App > div > span)
    
  447.       'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
    
  448.         '    in span (at **)\n' +
    
  449.         '    in div (at **)\n' +
    
  450.         '    in App (at **)',
    
  451.       // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
    
  452.       'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
    
  453.         '    in blink (at **)',
    
  454.       // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span)
    
  455.       'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
    
  456.         '    in span (at **)\n' +
    
  457.         '    in Child2 (at **)\n' +
    
  458.         '    in App2 (at **)',
    
  459.       // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span)
    
  460.       'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
    
  461.         '    in span (at **)\n' +
    
  462.         '    in Child (at **)',
    
  463.       // ReactDOM(App > div > font)
    
  464.       'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
    
  465.         '    in font (at **)\n' +
    
  466.         '    in div (at **)\n' +
    
  467.         '    in App (at **)',
    
  468.     ]);
    
  469.   });
    
  470. });