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('SimpleEventPlugin', function () {
    
  13.   let React;
    
  14.   let ReactDOM;
    
  15.   let ReactDOMClient;
    
  16.   let Scheduler;
    
  17.   let act;
    
  18. 
    
  19.   let onClick;
    
  20.   let container;
    
  21.   let assertLog;
    
  22.   let waitForAll;
    
  23. 
    
  24.   function expectClickThru(element) {
    
  25.     element.click();
    
  26.     expect(onClick).toHaveBeenCalledTimes(1);
    
  27.   }
    
  28. 
    
  29.   function expectNoClickThru(element) {
    
  30.     element.click();
    
  31.     expect(onClick).toHaveBeenCalledTimes(0);
    
  32.   }
    
  33. 
    
  34.   function mounted(element) {
    
  35.     container = document.createElement('div');
    
  36.     document.body.appendChild(container);
    
  37.     element = ReactDOM.render(element, container);
    
  38.     return element;
    
  39.   }
    
  40. 
    
  41.   beforeEach(function () {
    
  42.     jest.resetModules();
    
  43.     React = require('react');
    
  44.     ReactDOM = require('react-dom');
    
  45.     ReactDOMClient = require('react-dom/client');
    
  46.     Scheduler = require('scheduler');
    
  47. 
    
  48.     const InternalTestUtils = require('internal-test-utils');
    
  49.     assertLog = InternalTestUtils.assertLog;
    
  50.     waitForAll = InternalTestUtils.waitForAll;
    
  51. 
    
  52.     onClick = jest.fn();
    
  53.   });
    
  54. 
    
  55.   afterEach(() => {
    
  56.     if (container && document.body.contains(container)) {
    
  57.       document.body.removeChild(container);
    
  58.       container = null;
    
  59.     }
    
  60.   });
    
  61. 
    
  62.   it('A non-interactive tags click when disabled', function () {
    
  63.     const element = <div onClick={onClick} />;
    
  64.     expectClickThru(mounted(element));
    
  65.   });
    
  66. 
    
  67.   it('A non-interactive tags clicks bubble when disabled', function () {
    
  68.     const element = mounted(
    
  69.       <div onClick={onClick}>
    
  70.         <div />
    
  71.       </div>,
    
  72.     );
    
  73.     const child = element.firstChild;
    
  74.     child.click();
    
  75.     expect(onClick).toHaveBeenCalledTimes(1);
    
  76.   });
    
  77. 
    
  78.   it('does not register a click when clicking a child of a disabled element', function () {
    
  79.     const element = mounted(
    
  80.       <button onClick={onClick} disabled={true}>
    
  81.         <span />
    
  82.       </button>,
    
  83.     );
    
  84.     const child = element.querySelector('span');
    
  85. 
    
  86.     child.click();
    
  87.     expect(onClick).toHaveBeenCalledTimes(0);
    
  88.   });
    
  89. 
    
  90.   it('triggers click events for children of disabled elements', function () {
    
  91.     const element = mounted(
    
  92.       <button disabled={true}>
    
  93.         <span onClick={onClick} />
    
  94.       </button>,
    
  95.     );
    
  96.     const child = element.querySelector('span');
    
  97. 
    
  98.     child.click();
    
  99.     expect(onClick).toHaveBeenCalledTimes(1);
    
  100.   });
    
  101. 
    
  102.   it('triggers parent captured click events when target is a child of a disabled elements', function () {
    
  103.     const element = mounted(
    
  104.       <div onClickCapture={onClick}>
    
  105.         <button disabled={true}>
    
  106.           <span />
    
  107.         </button>
    
  108.       </div>,
    
  109.     );
    
  110.     const child = element.querySelector('span');
    
  111. 
    
  112.     child.click();
    
  113.     expect(onClick).toHaveBeenCalledTimes(1);
    
  114.   });
    
  115. 
    
  116.   it('triggers captured click events for children of disabled elements', function () {
    
  117.     const element = mounted(
    
  118.       <button disabled={true}>
    
  119.         <span onClickCapture={onClick} />
    
  120.       </button>,
    
  121.     );
    
  122.     const child = element.querySelector('span');
    
  123. 
    
  124.     child.click();
    
  125.     expect(onClick).toHaveBeenCalledTimes(1);
    
  126.   });
    
  127. 
    
  128.   ['button', 'input', 'select', 'textarea'].forEach(function (tagName) {
    
  129.     describe(tagName, function () {
    
  130.       it('should forward clicks when it starts out not disabled', () => {
    
  131.         const element = React.createElement(tagName, {
    
  132.           onClick: onClick,
    
  133.         });
    
  134. 
    
  135.         expectClickThru(mounted(element));
    
  136.       });
    
  137. 
    
  138.       it('should not forward clicks when it starts out disabled', () => {
    
  139.         const element = React.createElement(tagName, {
    
  140.           onClick: onClick,
    
  141.           disabled: true,
    
  142.         });
    
  143. 
    
  144.         expectNoClickThru(mounted(element));
    
  145.       });
    
  146. 
    
  147.       it('should forward clicks when it becomes not disabled', () => {
    
  148.         container = document.createElement('div');
    
  149.         document.body.appendChild(container);
    
  150.         let element = ReactDOM.render(
    
  151.           React.createElement(tagName, {onClick: onClick, disabled: true}),
    
  152.           container,
    
  153.         );
    
  154.         element = ReactDOM.render(
    
  155.           React.createElement(tagName, {onClick: onClick}),
    
  156.           container,
    
  157.         );
    
  158.         expectClickThru(element);
    
  159.       });
    
  160. 
    
  161.       it('should not forward clicks when it becomes disabled', () => {
    
  162.         container = document.createElement('div');
    
  163.         document.body.appendChild(container);
    
  164.         let element = ReactDOM.render(
    
  165.           React.createElement(tagName, {onClick: onClick}),
    
  166.           container,
    
  167.         );
    
  168.         element = ReactDOM.render(
    
  169.           React.createElement(tagName, {onClick: onClick, disabled: true}),
    
  170.           container,
    
  171.         );
    
  172.         expectNoClickThru(element);
    
  173.       });
    
  174. 
    
  175.       it('should work correctly if the listener is changed', () => {
    
  176.         container = document.createElement('div');
    
  177.         document.body.appendChild(container);
    
  178.         let element = ReactDOM.render(
    
  179.           React.createElement(tagName, {onClick: onClick, disabled: true}),
    
  180.           container,
    
  181.         );
    
  182.         element = ReactDOM.render(
    
  183.           React.createElement(tagName, {onClick: onClick, disabled: false}),
    
  184.           container,
    
  185.         );
    
  186.         expectClickThru(element);
    
  187.       });
    
  188.     });
    
  189.   });
    
  190. 
    
  191.   it('batches updates that occur as a result of a nested event dispatch', () => {
    
  192.     container = document.createElement('div');
    
  193.     document.body.appendChild(container);
    
  194. 
    
  195.     let button;
    
  196.     class Button extends React.Component {
    
  197.       state = {count: 0};
    
  198.       increment = () =>
    
  199.         this.setState(state => ({
    
  200.           count: state.count + 1,
    
  201.         }));
    
  202.       componentDidUpdate() {
    
  203.         Scheduler.log(`didUpdate - Count: ${this.state.count}`);
    
  204.       }
    
  205.       render() {
    
  206.         return (
    
  207.           <button
    
  208.             ref={el => (button = el)}
    
  209.             onFocus={this.increment}
    
  210.             onClick={() => {
    
  211.               // The focus call synchronously dispatches a nested event. All of
    
  212.               // the updates in this handler should be batched together.
    
  213.               this.increment();
    
  214.               button.focus();
    
  215.               this.increment();
    
  216.             }}>
    
  217.             Count: {this.state.count}
    
  218.           </button>
    
  219.         );
    
  220.       }
    
  221.     }
    
  222. 
    
  223.     function click() {
    
  224.       button.dispatchEvent(
    
  225.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  226.       );
    
  227.     }
    
  228. 
    
  229.     ReactDOM.render(<Button />, container);
    
  230.     expect(button.textContent).toEqual('Count: 0');
    
  231.     assertLog([]);
    
  232. 
    
  233.     click();
    
  234. 
    
  235.     // There should be exactly one update.
    
  236.     assertLog(['didUpdate - Count: 3']);
    
  237.     expect(button.textContent).toEqual('Count: 3');
    
  238.   });
    
  239. 
    
  240.   describe('interactive events, in concurrent mode', () => {
    
  241.     beforeEach(() => {
    
  242.       jest.resetModules();
    
  243. 
    
  244.       React = require('react');
    
  245.       ReactDOM = require('react-dom');
    
  246.       ReactDOMClient = require('react-dom/client');
    
  247.       Scheduler = require('scheduler');
    
  248. 
    
  249.       const InternalTestUtils = require('internal-test-utils');
    
  250.       assertLog = InternalTestUtils.assertLog;
    
  251.       waitForAll = InternalTestUtils.waitForAll;
    
  252. 
    
  253.       act = require('internal-test-utils').act;
    
  254.     });
    
  255. 
    
  256.     it('flushes pending interactive work before exiting event handler', async () => {
    
  257.       container = document.createElement('div');
    
  258.       const root = ReactDOMClient.createRoot(container);
    
  259.       document.body.appendChild(container);
    
  260. 
    
  261.       let button;
    
  262.       class Button extends React.Component {
    
  263.         state = {disabled: false};
    
  264.         onClick = () => {
    
  265.           // Perform some side-effect
    
  266.           Scheduler.log('Side-effect');
    
  267.           // Disable the button
    
  268.           this.setState({disabled: true});
    
  269.         };
    
  270.         render() {
    
  271.           Scheduler.log(
    
  272.             `render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
    
  273.           );
    
  274.           return (
    
  275.             <button
    
  276.               ref={el => (button = el)}
    
  277.               // Handler is removed after the first click
    
  278.               onClick={this.state.disabled ? null : this.onClick}
    
  279.             />
    
  280.           );
    
  281.         }
    
  282.       }
    
  283. 
    
  284.       // Initial mount
    
  285.       root.render(<Button />);
    
  286.       // Should not have flushed yet because it's async
    
  287.       assertLog([]);
    
  288.       expect(button).toBe(undefined);
    
  289.       // Flush async work
    
  290.       await waitForAll(['render button: enabled']);
    
  291. 
    
  292.       function click() {
    
  293.         const event = new MouseEvent('click', {
    
  294.           bubbles: true,
    
  295.           cancelable: true,
    
  296.         });
    
  297.         Object.defineProperty(event, 'timeStamp', {
    
  298.           value: 0,
    
  299.         });
    
  300.         button.dispatchEvent(event);
    
  301.       }
    
  302. 
    
  303.       // Click the button to trigger the side-effect
    
  304.       await act(() => click());
    
  305.       assertLog([
    
  306.         // The handler fired
    
  307.         'Side-effect',
    
  308.         // The component re-rendered synchronously, even in concurrent mode.
    
  309.         'render button: disabled',
    
  310.       ]);
    
  311. 
    
  312.       // Click the button again
    
  313.       click();
    
  314.       assertLog([
    
  315.         // The event handler was removed from the button, so there's no effect.
    
  316.       ]);
    
  317. 
    
  318.       // The handler should not fire again no matter how many times we
    
  319.       // click the handler.
    
  320.       click();
    
  321.       click();
    
  322.       click();
    
  323.       click();
    
  324.       click();
    
  325.       await waitForAll([]);
    
  326.     });
    
  327. 
    
  328.     // NOTE: This test was written for the old behavior of discrete updates,
    
  329.     // where they would be async, but flushed early if another discrete update
    
  330.     // was dispatched.
    
  331.     it('end result of many interactive updates is deterministic', async () => {
    
  332.       container = document.createElement('div');
    
  333.       const root = ReactDOMClient.createRoot(container);
    
  334.       document.body.appendChild(container);
    
  335. 
    
  336.       let button;
    
  337.       class Button extends React.Component {
    
  338.         state = {count: 0};
    
  339.         render() {
    
  340.           return (
    
  341.             <button
    
  342.               ref={el => (button = el)}
    
  343.               onClick={() =>
    
  344.                 // Intentionally not using the updater form here
    
  345.                 this.setState({count: this.state.count + 1})
    
  346.               }>
    
  347.               Count: {this.state.count}
    
  348.             </button>
    
  349.           );
    
  350.         }
    
  351.       }
    
  352. 
    
  353.       // Initial mount
    
  354.       root.render(<Button />);
    
  355.       // Should not have flushed yet because it's async
    
  356.       expect(button).toBe(undefined);
    
  357.       // Flush async work
    
  358.       await waitForAll([]);
    
  359.       expect(button.textContent).toEqual('Count: 0');
    
  360. 
    
  361.       function click() {
    
  362.         const event = new MouseEvent('click', {
    
  363.           bubbles: true,
    
  364.           cancelable: true,
    
  365.         });
    
  366.         Object.defineProperty(event, 'timeStamp', {
    
  367.           value: 0,
    
  368.         });
    
  369.         button.dispatchEvent(event);
    
  370.       }
    
  371. 
    
  372.       // Click the button a single time
    
  373.       await act(() => click());
    
  374.       // The counter should update synchronously, even in concurrent mode.
    
  375.       expect(button.textContent).toEqual('Count: 1');
    
  376. 
    
  377.       // Click the button many more times
    
  378.       await act(() => click());
    
  379.       await act(() => click());
    
  380.       await act(() => click());
    
  381.       await act(() => click());
    
  382.       await act(() => click());
    
  383.       await act(() => click());
    
  384. 
    
  385.       // Flush the remaining work
    
  386.       await waitForAll([]);
    
  387.       // The counter should equal the total number of clicks
    
  388.       expect(button.textContent).toEqual('Count: 7');
    
  389.     });
    
  390.   });
    
  391. 
    
  392.   describe('iOS bubbling click fix', function () {
    
  393.     // See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
    
  394. 
    
  395.     it('does not add a local click to interactive elements', function () {
    
  396.       container = document.createElement('div');
    
  397. 
    
  398.       ReactDOM.render(<button onClick={onClick} />, container);
    
  399. 
    
  400.       const node = container.firstChild;
    
  401. 
    
  402.       node.dispatchEvent(new MouseEvent('click'));
    
  403. 
    
  404.       expect(onClick).toHaveBeenCalledTimes(0);
    
  405.     });
    
  406. 
    
  407.     it('adds a local click listener to non-interactive elements', function () {
    
  408.       container = document.createElement('div');
    
  409. 
    
  410.       ReactDOM.render(<div onClick={onClick} />, container);
    
  411. 
    
  412.       const node = container.firstChild;
    
  413. 
    
  414.       node.dispatchEvent(new MouseEvent('click'));
    
  415. 
    
  416.       expect(onClick).toHaveBeenCalledTimes(0);
    
  417.     });
    
  418. 
    
  419.     it('registers passive handlers for events affected by the intervention', () => {
    
  420.       container = document.createElement('div');
    
  421. 
    
  422.       const passiveEvents = [];
    
  423.       const nativeAddEventListener = container.addEventListener;
    
  424.       container.addEventListener = function (type, fn, options) {
    
  425.         if (options !== null && typeof options === 'object') {
    
  426.           if (options.passive) {
    
  427.             passiveEvents.push(type);
    
  428.           }
    
  429.         }
    
  430.         return nativeAddEventListener.apply(this, arguments);
    
  431.       };
    
  432. 
    
  433.       ReactDOM.render(<div />, container);
    
  434. 
    
  435.       expect(passiveEvents).toEqual([
    
  436.         'touchstart',
    
  437.         'touchstart',
    
  438.         'touchmove',
    
  439.         'touchmove',
    
  440.         'wheel',
    
  441.         'wheel',
    
  442.       ]);
    
  443.     });
    
  444.   });
    
  445. });