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. 
    
  15. const ChildComponent = ({id, eventHandler}) => (
    
  16.   <div
    
  17.     id={id + '__DIV'}
    
  18.     onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  19.     onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  20.     onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  21.     onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}>
    
  22.     <div
    
  23.       id={id + '__DIV_1'}
    
  24.       onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  25.       onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  26.       onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  27.       onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}
    
  28.     />
    
  29.     <div
    
  30.       id={id + '__DIV_2'}
    
  31.       onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  32.       onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  33.       onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  34.       onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}
    
  35.     />
    
  36.   </div>
    
  37. );
    
  38. 
    
  39. const ParentComponent = ({eventHandler}) => (
    
  40.   <div
    
  41.     id="P"
    
  42.     onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  43.     onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  44.     onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  45.     onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}>
    
  46.     <div
    
  47.       id="P_P1"
    
  48.       onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  49.       onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  50.       onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  51.       onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}>
    
  52.       <ChildComponent id="P_P1_C1" eventHandler={eventHandler} />
    
  53.       <ChildComponent id="P_P1_C2" eventHandler={eventHandler} />
    
  54.     </div>
    
  55.     <div
    
  56.       id="P_OneOff"
    
  57.       onClickCapture={e => eventHandler(e.currentTarget.id, 'captured', e.type)}
    
  58.       onClick={e => eventHandler(e.currentTarget.id, 'bubbled', e.type)}
    
  59.       onMouseEnter={e => eventHandler(e.currentTarget.id, e.type)}
    
  60.       onMouseLeave={e => eventHandler(e.currentTarget.id, e.type)}
    
  61.     />
    
  62.   </div>
    
  63. );
    
  64. 
    
  65. describe('ReactTreeTraversal', () => {
    
  66.   const mockFn = jest.fn();
    
  67.   let container;
    
  68.   let outerNode1;
    
  69.   let outerNode2;
    
  70. 
    
  71.   beforeEach(() => {
    
  72.     React = require('react');
    
  73.     ReactDOM = require('react-dom');
    
  74. 
    
  75.     mockFn.mockReset();
    
  76. 
    
  77.     container = document.createElement('div');
    
  78.     outerNode1 = document.createElement('div');
    
  79.     outerNode2 = document.createElement('div');
    
  80.     document.body.appendChild(container);
    
  81.     document.body.appendChild(outerNode1);
    
  82.     document.body.appendChild(outerNode2);
    
  83. 
    
  84.     ReactDOM.render(<ParentComponent eventHandler={mockFn} />, container);
    
  85.   });
    
  86. 
    
  87.   afterEach(() => {
    
  88.     document.body.removeChild(container);
    
  89.     document.body.removeChild(outerNode1);
    
  90.     document.body.removeChild(outerNode2);
    
  91.     container = null;
    
  92.     outerNode1 = null;
    
  93.     outerNode2 = null;
    
  94.   });
    
  95. 
    
  96.   describe('Two phase traversal', () => {
    
  97.     it('should not traverse when target is outside component boundary', () => {
    
  98.       outerNode1.dispatchEvent(
    
  99.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  100.       );
    
  101. 
    
  102.       expect(mockFn).not.toHaveBeenCalled();
    
  103.     });
    
  104. 
    
  105.     it('should traverse two phase across component boundary', () => {
    
  106.       const expectedCalls = [
    
  107.         ['P', 'captured', 'click'],
    
  108.         ['P_P1', 'captured', 'click'],
    
  109.         ['P_P1_C1__DIV', 'captured', 'click'],
    
  110.         ['P_P1_C1__DIV_1', 'captured', 'click'],
    
  111. 
    
  112.         ['P_P1_C1__DIV_1', 'bubbled', 'click'],
    
  113.         ['P_P1_C1__DIV', 'bubbled', 'click'],
    
  114.         ['P_P1', 'bubbled', 'click'],
    
  115.         ['P', 'bubbled', 'click'],
    
  116.       ];
    
  117. 
    
  118.       const node = document.getElementById('P_P1_C1__DIV_1');
    
  119.       node.dispatchEvent(
    
  120.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  121.       );
    
  122. 
    
  123.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  124.     });
    
  125. 
    
  126.     it('should traverse two phase at shallowest node', () => {
    
  127.       const node = document.getElementById('P');
    
  128.       node.dispatchEvent(
    
  129.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  130.       );
    
  131. 
    
  132.       const expectedCalls = [
    
  133.         ['P', 'captured', 'click'],
    
  134.         ['P', 'bubbled', 'click'],
    
  135.       ];
    
  136.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  137.     });
    
  138.   });
    
  139. 
    
  140.   describe('Enter leave traversal', () => {
    
  141.     it('should not traverse when enter/leaving outside DOM', () => {
    
  142.       outerNode1.dispatchEvent(
    
  143.         new MouseEvent('mouseout', {
    
  144.           bubbles: true,
    
  145.           cancelable: true,
    
  146.           relatedTarget: outerNode2,
    
  147.         }),
    
  148.       );
    
  149. 
    
  150.       expect(mockFn).not.toHaveBeenCalled();
    
  151.     });
    
  152. 
    
  153.     it('should not traverse if enter/leave the same node', () => {
    
  154.       const leaveNode = document.getElementById('P_P1_C1__DIV_1');
    
  155.       const enterNode = document.getElementById('P_P1_C1__DIV_1');
    
  156. 
    
  157.       leaveNode.dispatchEvent(
    
  158.         new MouseEvent('mouseout', {
    
  159.           bubbles: true,
    
  160.           cancelable: true,
    
  161.           relatedTarget: enterNode,
    
  162.         }),
    
  163.       );
    
  164. 
    
  165.       expect(mockFn).not.toHaveBeenCalled();
    
  166.     });
    
  167. 
    
  168.     it('should traverse enter/leave to sibling - avoids parent', () => {
    
  169.       const leaveNode = document.getElementById('P_P1_C1__DIV_1');
    
  170.       const enterNode = document.getElementById('P_P1_C1__DIV_2');
    
  171. 
    
  172.       const expectedCalls = [
    
  173.         ['P_P1_C1__DIV_1', 'mouseleave'],
    
  174.         // enter/leave shouldn't fire anything on the parent
    
  175.         ['P_P1_C1__DIV_2', 'mouseenter'],
    
  176.       ];
    
  177. 
    
  178.       leaveNode.dispatchEvent(
    
  179.         new MouseEvent('mouseout', {
    
  180.           bubbles: true,
    
  181.           cancelable: true,
    
  182.           relatedTarget: enterNode,
    
  183.         }),
    
  184.       );
    
  185. 
    
  186.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  187.     });
    
  188. 
    
  189.     it('should traverse enter/leave to parent - avoids parent', () => {
    
  190.       const leaveNode = document.getElementById('P_P1_C1__DIV_1');
    
  191.       const enterNode = document.getElementById('P_P1_C1__DIV');
    
  192. 
    
  193.       const expectedCalls = [['P_P1_C1__DIV_1', 'mouseleave']];
    
  194. 
    
  195.       leaveNode.dispatchEvent(
    
  196.         new MouseEvent('mouseout', {
    
  197.           bubbles: true,
    
  198.           cancelable: true,
    
  199.           relatedTarget: enterNode,
    
  200.         }),
    
  201.       );
    
  202. 
    
  203.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  204.     });
    
  205. 
    
  206.     // The modern event system attaches event listeners to roots so the
    
  207.     // event below is being triggered on a node that React does not listen
    
  208.     // to any more. Instead we should fire mouseover.
    
  209.     it('should enter from the window', () => {
    
  210.       const enterNode = document.getElementById('P_P1_C1__DIV');
    
  211. 
    
  212.       const expectedCalls = [
    
  213.         ['P', 'mouseenter'],
    
  214.         ['P_P1', 'mouseenter'],
    
  215.         ['P_P1_C1__DIV', 'mouseenter'],
    
  216.       ];
    
  217. 
    
  218.       enterNode.dispatchEvent(
    
  219.         new MouseEvent('mouseover', {
    
  220.           bubbles: true,
    
  221.           cancelable: true,
    
  222.           relatedTarget: outerNode1,
    
  223.         }),
    
  224.       );
    
  225. 
    
  226.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  227.     });
    
  228. 
    
  229.     it('should enter from the window to the shallowest', () => {
    
  230.       const enterNode = document.getElementById('P');
    
  231. 
    
  232.       const expectedCalls = [['P', 'mouseenter']];
    
  233. 
    
  234.       enterNode.dispatchEvent(
    
  235.         new MouseEvent('mouseover', {
    
  236.           bubbles: true,
    
  237.           cancelable: true,
    
  238.           relatedTarget: outerNode1,
    
  239.         }),
    
  240.       );
    
  241. 
    
  242.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  243.     });
    
  244. 
    
  245.     it('should leave to the window', () => {
    
  246.       const leaveNode = document.getElementById('P_P1_C1__DIV');
    
  247. 
    
  248.       const expectedCalls = [
    
  249.         ['P_P1_C1__DIV', 'mouseleave'],
    
  250.         ['P_P1', 'mouseleave'],
    
  251.         ['P', 'mouseleave'],
    
  252.       ];
    
  253. 
    
  254.       leaveNode.dispatchEvent(
    
  255.         new MouseEvent('mouseout', {
    
  256.           bubbles: true,
    
  257.           cancelable: true,
    
  258.           relatedTarget: outerNode1,
    
  259.         }),
    
  260.       );
    
  261. 
    
  262.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  263.     });
    
  264. 
    
  265.     it('should leave to the window from the shallowest', () => {
    
  266.       const leaveNode = document.getElementById('P');
    
  267. 
    
  268.       const expectedCalls = [['P', 'mouseleave']];
    
  269. 
    
  270.       leaveNode.dispatchEvent(
    
  271.         new MouseEvent('mouseout', {
    
  272.           bubbles: true,
    
  273.           cancelable: true,
    
  274.           relatedTarget: outerNode1,
    
  275.         }),
    
  276.       );
    
  277. 
    
  278.       expect(mockFn.mock.calls).toEqual(expectedCalls);
    
  279.     });
    
  280.   });
    
  281. });