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. 
    
  14. let ReactDOM;
    
  15. let ReactDOMClient;
    
  16. let Scheduler;
    
  17. let act;
    
  18. let assertLog;
    
  19. let waitFor;
    
  20. 
    
  21. describe('ReactDOMNativeEventHeuristic-test', () => {
    
  22.   let container;
    
  23. 
    
  24.   beforeEach(() => {
    
  25.     jest.resetModules();
    
  26.     container = document.createElement('div');
    
  27.     React = require('react');
    
  28.     ReactDOM = require('react-dom');
    
  29.     ReactDOMClient = require('react-dom/client');
    
  30.     Scheduler = require('scheduler');
    
  31.     act = require('internal-test-utils').act;
    
  32. 
    
  33.     const InternalTestUtils = require('internal-test-utils');
    
  34.     assertLog = InternalTestUtils.assertLog;
    
  35.     waitFor = InternalTestUtils.waitFor;
    
  36. 
    
  37.     document.body.appendChild(container);
    
  38.   });
    
  39. 
    
  40.   afterEach(() => {
    
  41.     document.body.removeChild(container);
    
  42.   });
    
  43. 
    
  44.   function dispatchAndSetCurrentEvent(el, event) {
    
  45.     try {
    
  46.       window.event = event;
    
  47.       el.dispatchEvent(event);
    
  48.     } finally {
    
  49.       window.event = undefined;
    
  50.     }
    
  51.   }
    
  52. 
    
  53.   it('ignores discrete events on a pending removed element', async () => {
    
  54.     const disableButtonRef = React.createRef();
    
  55.     const submitButtonRef = React.createRef();
    
  56. 
    
  57.     function Form() {
    
  58.       const [active, setActive] = React.useState(true);
    
  59. 
    
  60.       React.useLayoutEffect(() => {
    
  61.         disableButtonRef.current.onclick = disableForm;
    
  62.       });
    
  63. 
    
  64.       function disableForm() {
    
  65.         setActive(false);
    
  66.       }
    
  67. 
    
  68.       return (
    
  69.         <div>
    
  70.           <button ref={disableButtonRef}>Disable</button>
    
  71.           {active ? <button ref={submitButtonRef}>Submit</button> : null}
    
  72.         </div>
    
  73.       );
    
  74.     }
    
  75. 
    
  76.     const root = ReactDOMClient.createRoot(container);
    
  77.     await act(() => {
    
  78.       root.render(<Form />);
    
  79.     });
    
  80. 
    
  81.     const disableButton = disableButtonRef.current;
    
  82.     expect(disableButton.tagName).toBe('BUTTON');
    
  83. 
    
  84.     // Dispatch a click event on the Disable-button.
    
  85.     await act(async () => {
    
  86.       const firstEvent = document.createEvent('Event');
    
  87.       firstEvent.initEvent('click', true, true);
    
  88.       dispatchAndSetCurrentEvent(disableButton, firstEvent);
    
  89.     });
    
  90.     // Discrete events should be flushed in a microtask.
    
  91.     // Verify that the second button was removed.
    
  92.     expect(submitButtonRef.current).toBe(null);
    
  93.     // We'll assume that the browser won't let the user click it.
    
  94.   });
    
  95. 
    
  96.   it('ignores discrete events on a pending removed event listener', async () => {
    
  97.     const disableButtonRef = React.createRef();
    
  98.     const submitButtonRef = React.createRef();
    
  99. 
    
  100.     let formSubmitted = false;
    
  101. 
    
  102.     function Form() {
    
  103.       const [active, setActive] = React.useState(true);
    
  104. 
    
  105.       React.useLayoutEffect(() => {
    
  106.         disableButtonRef.current.onclick = disableForm;
    
  107.         submitButtonRef.current.onclick = active
    
  108.           ? submitForm
    
  109.           : disabledSubmitForm;
    
  110.       });
    
  111. 
    
  112.       function disableForm() {
    
  113.         setActive(false);
    
  114.       }
    
  115. 
    
  116.       function submitForm() {
    
  117.         formSubmitted = true; // This should not get invoked
    
  118.       }
    
  119. 
    
  120.       function disabledSubmitForm() {
    
  121.         // The form is disabled.
    
  122.       }
    
  123. 
    
  124.       return (
    
  125.         <div>
    
  126.           <button ref={disableButtonRef}>Disable</button>
    
  127.           <button ref={submitButtonRef}>Submit</button>
    
  128.         </div>
    
  129.       );
    
  130.     }
    
  131. 
    
  132.     const root = ReactDOMClient.createRoot(container);
    
  133.     // Flush
    
  134.     await act(() => root.render(<Form />));
    
  135. 
    
  136.     const disableButton = disableButtonRef.current;
    
  137.     expect(disableButton.tagName).toBe('BUTTON');
    
  138. 
    
  139.     // Dispatch a click event on the Disable-button.
    
  140.     const firstEvent = document.createEvent('Event');
    
  141.     firstEvent.initEvent('click', true, true);
    
  142.     await act(() => {
    
  143.       dispatchAndSetCurrentEvent(disableButton, firstEvent);
    
  144. 
    
  145.       // There should now be a pending update to disable the form.
    
  146.       // This should not have flushed yet since it's in concurrent mode.
    
  147.       const submitButton = submitButtonRef.current;
    
  148.       expect(submitButton.tagName).toBe('BUTTON');
    
  149. 
    
  150.       // Flush the discrete event
    
  151.       ReactDOM.flushSync();
    
  152. 
    
  153.       // Now let's dispatch an event on the submit button.
    
  154.       const secondEvent = document.createEvent('Event');
    
  155.       secondEvent.initEvent('click', true, true);
    
  156.       dispatchAndSetCurrentEvent(submitButton, secondEvent);
    
  157.     });
    
  158. 
    
  159.     // Therefore the form should never have been submitted.
    
  160.     expect(formSubmitted).toBe(false);
    
  161.   });
    
  162. 
    
  163.   it('uses the newest discrete events on a pending changed event listener', async () => {
    
  164.     const enableButtonRef = React.createRef();
    
  165.     const submitButtonRef = React.createRef();
    
  166. 
    
  167.     let formSubmitted = false;
    
  168. 
    
  169.     function Form() {
    
  170.       const [active, setActive] = React.useState(false);
    
  171. 
    
  172.       React.useLayoutEffect(() => {
    
  173.         enableButtonRef.current.onclick = enableForm;
    
  174.         submitButtonRef.current.onclick = active ? submitForm : null;
    
  175.       });
    
  176. 
    
  177.       function enableForm() {
    
  178.         setActive(true);
    
  179.       }
    
  180. 
    
  181.       function submitForm() {
    
  182.         formSubmitted = true; // This should not get invoked
    
  183.       }
    
  184. 
    
  185.       return (
    
  186.         <div>
    
  187.           <button ref={enableButtonRef}>Enable</button>
    
  188.           <button ref={submitButtonRef}>Submit</button>
    
  189.         </div>
    
  190.       );
    
  191.     }
    
  192. 
    
  193.     const root = ReactDOMClient.createRoot(container);
    
  194.     await act(() => root.render(<Form />));
    
  195. 
    
  196.     const enableButton = enableButtonRef.current;
    
  197.     expect(enableButton.tagName).toBe('BUTTON');
    
  198. 
    
  199.     // Dispatch a click event on the Enable-button.
    
  200.     await act(() => {
    
  201.       const firstEvent = document.createEvent('Event');
    
  202.       firstEvent.initEvent('click', true, true);
    
  203.       dispatchAndSetCurrentEvent(enableButton, firstEvent);
    
  204. 
    
  205.       // There should now be a pending update to enable the form.
    
  206.       // This should not have flushed yet since it's in concurrent mode.
    
  207.       const submitButton = submitButtonRef.current;
    
  208.       expect(submitButton.tagName).toBe('BUTTON');
    
  209. 
    
  210.       // Flush discrete updates
    
  211.       ReactDOM.flushSync();
    
  212. 
    
  213.       // Now let's dispatch an event on the submit button.
    
  214.       const secondEvent = document.createEvent('Event');
    
  215.       secondEvent.initEvent('click', true, true);
    
  216.       dispatchAndSetCurrentEvent(submitButton, secondEvent);
    
  217.     });
    
  218. 
    
  219.     // Therefore the form should have been submitted.
    
  220.     expect(formSubmitted).toBe(true);
    
  221.   });
    
  222. 
    
  223.   it('mouse over should be user-blocking but not discrete', async () => {
    
  224.     const root = ReactDOMClient.createRoot(container);
    
  225. 
    
  226.     const target = React.createRef(null);
    
  227.     function Foo() {
    
  228.       const [isHover, setHover] = React.useState(false);
    
  229.       React.useLayoutEffect(() => {
    
  230.         target.current.onmouseover = () => setHover(true);
    
  231.       });
    
  232.       return <div ref={target}>{isHover ? 'hovered' : 'not hovered'}</div>;
    
  233.     }
    
  234. 
    
  235.     await act(() => {
    
  236.       root.render(<Foo />);
    
  237.     });
    
  238.     expect(container.textContent).toEqual('not hovered');
    
  239. 
    
  240.     await act(() => {
    
  241.       const mouseOverEvent = document.createEvent('MouseEvents');
    
  242.       mouseOverEvent.initEvent('mouseover', true, true);
    
  243.       dispatchAndSetCurrentEvent(target.current, mouseOverEvent);
    
  244. 
    
  245.       // Flush discrete updates
    
  246.       ReactDOM.flushSync();
    
  247.       // Since mouse over is not discrete, should not have updated yet
    
  248.       expect(container.textContent).toEqual('not hovered');
    
  249.     });
    
  250.     expect(container.textContent).toEqual('hovered');
    
  251.   });
    
  252. 
    
  253.   it('mouse enter should be user-blocking but not discrete', async () => {
    
  254.     const root = ReactDOMClient.createRoot(container);
    
  255. 
    
  256.     const target = React.createRef(null);
    
  257.     function Foo() {
    
  258.       const [isHover, setHover] = React.useState(false);
    
  259.       React.useLayoutEffect(() => {
    
  260.         target.current.onmouseenter = () => setHover(true);
    
  261.       });
    
  262.       return <div ref={target}>{isHover ? 'hovered' : 'not hovered'}</div>;
    
  263.     }
    
  264. 
    
  265.     await act(() => {
    
  266.       root.render(<Foo />);
    
  267.     });
    
  268.     expect(container.textContent).toEqual('not hovered');
    
  269. 
    
  270.     await act(() => {
    
  271.       // Note: React does not use native mouseenter/mouseleave events
    
  272.       // but we should still correctly determine their priority.
    
  273.       const mouseEnterEvent = document.createEvent('MouseEvents');
    
  274.       mouseEnterEvent.initEvent('mouseenter', true, true);
    
  275.       dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
    
  276. 
    
  277.       // Flush discrete updates
    
  278.       ReactDOM.flushSync();
    
  279.       // Since mouse end is not discrete, should not have updated yet
    
  280.       expect(container.textContent).toEqual('not hovered');
    
  281.     });
    
  282.     expect(container.textContent).toEqual('hovered');
    
  283.   });
    
  284. 
    
  285.   it('continuous native events flush as expected', async () => {
    
  286.     const root = ReactDOMClient.createRoot(container);
    
  287. 
    
  288.     const target = React.createRef(null);
    
  289.     function Foo({hovered}) {
    
  290.       const hoverString = hovered ? 'hovered' : 'not hovered';
    
  291.       Scheduler.log(hoverString);
    
  292.       return <div ref={target}>{hoverString}</div>;
    
  293.     }
    
  294. 
    
  295.     await act(() => {
    
  296.       root.render(<Foo hovered={false} />);
    
  297.     });
    
  298.     expect(container.textContent).toEqual('not hovered');
    
  299. 
    
  300.     await act(async () => {
    
  301.       // Note: React does not use native mouseenter/mouseleave events
    
  302.       // but we should still correctly determine their priority.
    
  303.       const mouseEnterEvent = document.createEvent('MouseEvents');
    
  304.       mouseEnterEvent.initEvent('mouseover', true, true);
    
  305.       target.current.addEventListener('mouseover', () => {
    
  306.         root.render(<Foo hovered={true} />);
    
  307.       });
    
  308.       dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
    
  309. 
    
  310.       // Since mouse end is not discrete, should not have updated yet
    
  311.       assertLog(['not hovered']);
    
  312.       expect(container.textContent).toEqual('not hovered');
    
  313. 
    
  314.       await waitFor(['hovered']);
    
  315.       if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
    
  316.         expect(container.textContent).toEqual('not hovered');
    
  317.       } else {
    
  318.         expect(container.textContent).toEqual('hovered');
    
  319.       }
    
  320.     });
    
  321.     expect(container.textContent).toEqual('hovered');
    
  322.   });
    
  323. 
    
  324.   it('should batch inside native events', async () => {
    
  325.     const root = ReactDOMClient.createRoot(container);
    
  326. 
    
  327.     const target = React.createRef(null);
    
  328.     function Foo() {
    
  329.       const [count, setCount] = React.useState(0);
    
  330.       const countRef = React.useRef(-1);
    
  331. 
    
  332.       React.useLayoutEffect(() => {
    
  333.         countRef.current = count;
    
  334.         target.current.onclick = () => {
    
  335.           setCount(countRef.current + 1);
    
  336.           // Now update again. If these updates are batched, then this should be
    
  337.           // a no-op, because we didn't re-render yet and `countRef` hasn't
    
  338.           // been mutated.
    
  339.           setCount(countRef.current + 1);
    
  340.         };
    
  341.       });
    
  342.       return <div ref={target}>Count: {count}</div>;
    
  343.     }
    
  344. 
    
  345.     await act(() => {
    
  346.       root.render(<Foo />);
    
  347.     });
    
  348.     expect(container.textContent).toEqual('Count: 0');
    
  349. 
    
  350.     await act(async () => {
    
  351.       const pressEvent = document.createEvent('Event');
    
  352.       pressEvent.initEvent('click', true, true);
    
  353.       dispatchAndSetCurrentEvent(target.current, pressEvent);
    
  354.     });
    
  355.     // If this is 2, that means the `setCount` calls were not batched.
    
  356.     expect(container.textContent).toEqual('Count: 1');
    
  357.   });
    
  358. 
    
  359.   it('should not flush discrete events at the end of outermost batchedUpdates', async () => {
    
  360.     const root = ReactDOMClient.createRoot(container);
    
  361. 
    
  362.     let target;
    
  363.     function Foo() {
    
  364.       const [count, setCount] = React.useState(0);
    
  365.       return (
    
  366.         <div
    
  367.           ref={el => {
    
  368.             target = el;
    
  369.             if (target !== null) {
    
  370.               el.onclick = () => {
    
  371.                 ReactDOM.unstable_batchedUpdates(() => {
    
  372.                   setCount(count + 1);
    
  373.                 });
    
  374.                 Scheduler.log(
    
  375.                   container.textContent + ' [after batchedUpdates]',
    
  376.                 );
    
  377.               };
    
  378.             }
    
  379.           }}>
    
  380.           Count: {count}
    
  381.         </div>
    
  382.       );
    
  383.     }
    
  384. 
    
  385.     await act(() => {
    
  386.       root.render(<Foo />);
    
  387.     });
    
  388.     expect(container.textContent).toEqual('Count: 0');
    
  389. 
    
  390.     await act(async () => {
    
  391.       const pressEvent = document.createEvent('Event');
    
  392.       pressEvent.initEvent('click', true, true);
    
  393.       dispatchAndSetCurrentEvent(target, pressEvent);
    
  394.       assertLog(['Count: 0 [after batchedUpdates]']);
    
  395.       expect(container.textContent).toEqual('Count: 0');
    
  396.     });
    
  397.     expect(container.textContent).toEqual('Count: 1');
    
  398.   });
    
  399. });