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