main
js 358 lines 9.82 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
16 let idCallOrder;
17 const recordID = function (id) {
18 idCallOrder.push(id);
19 };
20 const recordIDAndStopPropagation = function (id, event) {
21 recordID(id);
22 event.stopPropagation();
23 };
24 const recordIDAndReturnFalse = function (id, event) {
25 recordID(id);
26 return false;
27 };
28 const LISTENER = jest.fn();
29 const ON_CLICK_KEY = 'onClick';
30
31 let GRANDPARENT;
32 let PARENT;
33 let CHILD;
34 let BUTTON;
35
36 let renderTree;
37 let putListener;
38 let deleteAllListeners;
39
40 let container;
41
42 // This test is written in a bizarre way because it was previously using internals.
43 // It should probably be rewritten but we're keeping it for some extra coverage.
44 describe('ReactBrowserEventEmitter', () => {
45 beforeEach(() => {
46 jest.resetModules();
47 LISTENER.mockClear();
48
49 React = require('react');
50 ReactDOMClient = require('react-dom/client');
51 act = require('internal-test-utils').act;
52 container = document.createElement('div');
53 document.body.appendChild(container);
54
55 let GRANDPARENT_PROPS = {};
56 let PARENT_PROPS = {};
57 let CHILD_PROPS = {};
58 let BUTTON_PROPS = {};
59
60 function Child(props) {
61 return <div ref={c => (CHILD = c)} {...props} />;
62 }
63
64 class ChildWrapper extends React.PureComponent {
65 render() {
66 return <Child {...this.props} />;
67 }
68 }
69
70 const root = ReactDOMClient.createRoot(container);
71
72 renderTree = async function () {
73 await act(() => {
74 root.render(
75 <div ref={c => (GRANDPARENT = c)} {...GRANDPARENT_PROPS}>
76 <div ref={c => (PARENT = c)} {...PARENT_PROPS}>
77 <ChildWrapper {...CHILD_PROPS} />
78 <button
79 disabled={true}
80 ref={c => (BUTTON = c)}
81 {...BUTTON_PROPS}
82 />
83 </div>
84 </div>,
85 );
86 });
87 };
88
89 putListener = async function (node, eventName, listener) {
90 switch (node) {
91 case CHILD:
92 CHILD_PROPS[eventName] = listener;
93 break;
94 case PARENT:
95 PARENT_PROPS[eventName] = listener;
96 break;
97 case GRANDPARENT:
98 GRANDPARENT_PROPS[eventName] = listener;
99 break;
100 case BUTTON:
101 BUTTON_PROPS[eventName] = listener;
102 break;
103 }
104 // Rerender with new event listeners
105 await renderTree();
106 };
107
108 deleteAllListeners = async function (node) {
109 switch (node) {
110 case CHILD:
111 CHILD_PROPS = {};
112 break;
113 case PARENT:
114 PARENT_PROPS = {};
115 break;
116 case GRANDPARENT:
117 GRANDPARENT_PROPS = {};
118 break;
119 case BUTTON:
120 BUTTON_PROPS = {};
121 break;
122 }
123 await renderTree();
124 };
125
126 idCallOrder = [];
127 });
128
129 afterEach(() => {
130 document.body.removeChild(container);
131 container = null;
132 });
133
134 it('should bubble simply', async () => {
135 await renderTree();
136 await putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
137 await putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
138 await putListener(
139 GRANDPARENT,
140 ON_CLICK_KEY,
141 recordID.bind(null, GRANDPARENT),
142 );
143 await act(() => {
144 CHILD.click();
145 });
146 expect(idCallOrder.length).toBe(3);
147 expect(idCallOrder[0]).toBe(CHILD);
148 expect(idCallOrder[1]).toBe(PARENT);
149 expect(idCallOrder[2]).toBe(GRANDPARENT);
150 });
151
152 it('should bubble to the right handler after an update', async () => {
153 await renderTree();
154 await putListener(
155 GRANDPARENT,
156 ON_CLICK_KEY,
157 recordID.bind(null, 'GRANDPARENT'),
158 );
159 await putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, 'PARENT'));
160 await putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, 'CHILD'));
161 await act(() => {
162 CHILD.click();
163 });
164 expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'GRANDPARENT']);
165
166 idCallOrder = [];
167
168 // Update just the grand parent without updating the child.
169 await putListener(
170 GRANDPARENT,
171 ON_CLICK_KEY,
172 recordID.bind(null, 'UPDATED_GRANDPARENT'),
173 );
174
175 await act(() => {
176 CHILD.click();
177 });
178 expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'UPDATED_GRANDPARENT']);
179 });
180
181 it('should continue bubbling if an error is thrown', async () => {
182 await renderTree();
183 await putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
184 await putListener(PARENT, ON_CLICK_KEY, function (event) {
185 recordID(PARENT);
186 throw new Error('Handler interrupted');
187 });
188 await putListener(
189 GRANDPARENT,
190 ON_CLICK_KEY,
191 recordID.bind(null, GRANDPARENT),
192 );
193 const errorHandler = jest.fn(event => {
194 event.preventDefault();
195 });
196 window.addEventListener('error', errorHandler);
197 try {
198 CHILD.click();
199 expect(idCallOrder.length).toBe(3);
200 expect(idCallOrder[0]).toBe(CHILD);
201 expect(idCallOrder[1]).toBe(PARENT);
202 expect(idCallOrder[2]).toBe(GRANDPARENT);
203 expect(errorHandler).toHaveBeenCalledTimes(1);
204 expect(errorHandler.mock.calls[0][0]).toEqual(
205 expect.objectContaining({
206 error: expect.any(Error),
207 message: 'Handler interrupted',
208 }),
209 );
210 } finally {
211 window.removeEventListener('error', errorHandler);
212 }
213 });
214
215 it('should set currentTarget', async () => {
216 await renderTree();
217 await putListener(CHILD, ON_CLICK_KEY, function (event) {
218 recordID(CHILD);
219 expect(event.currentTarget).toBe(CHILD);
220 });
221 await putListener(PARENT, ON_CLICK_KEY, function (event) {
222 recordID(PARENT);
223 expect(event.currentTarget).toBe(PARENT);
224 });
225 await putListener(GRANDPARENT, ON_CLICK_KEY, function (event) {
226 recordID(GRANDPARENT);
227 expect(event.currentTarget).toBe(GRANDPARENT);
228 });
229 await act(() => {
230 CHILD.click();
231 });
232 expect(idCallOrder.length).toBe(3);
233 expect(idCallOrder[0]).toBe(CHILD);
234 expect(idCallOrder[1]).toBe(PARENT);
235 expect(idCallOrder[2]).toBe(GRANDPARENT);
236 });
237
238 it('should support stopPropagation()', async () => {
239 await renderTree();
240 await putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
241 await putListener(
242 PARENT,
243 ON_CLICK_KEY,
244 recordIDAndStopPropagation.bind(null, PARENT),
245 );
246 await putListener(
247 GRANDPARENT,
248 ON_CLICK_KEY,
249 recordID.bind(null, GRANDPARENT),
250 );
251 await act(() => {
252 CHILD.click();
253 });
254 expect(idCallOrder.length).toBe(2);
255 expect(idCallOrder[0]).toBe(CHILD);
256 expect(idCallOrder[1]).toBe(PARENT);
257 });
258
259 it('should support overriding .isPropagationStopped()', async () => {
260 await renderTree();
261 // Ew. See D4504876.
262 await putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
263 await putListener(PARENT, ON_CLICK_KEY, function (e) {
264 recordID(PARENT, e);
265 // This stops React bubbling but avoids touching the native event
266 e.isPropagationStopped = () => true;
267 });
268 await putListener(
269 GRANDPARENT,
270 ON_CLICK_KEY,
271 recordID.bind(null, GRANDPARENT),
272 );
273 await act(() => {
274 CHILD.click();
275 });
276 expect(idCallOrder.length).toBe(2);
277 expect(idCallOrder[0]).toBe(CHILD);
278 expect(idCallOrder[1]).toBe(PARENT);
279 });
280
281 it('should stop after first dispatch if stopPropagation', async () => {
282 await renderTree();
283 await putListener(
284 CHILD,
285 ON_CLICK_KEY,
286 recordIDAndStopPropagation.bind(null, CHILD),
287 );
288 await putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
289 await putListener(
290 GRANDPARENT,
291 ON_CLICK_KEY,
292 recordID.bind(null, GRANDPARENT),
293 );
294 await act(() => {
295 CHILD.click();
296 });
297 expect(idCallOrder.length).toBe(1);
298 expect(idCallOrder[0]).toBe(CHILD);
299 });
300
301 it('should not stopPropagation if false is returned', async () => {
302 await renderTree();
303 await putListener(
304 CHILD,
305 ON_CLICK_KEY,
306 recordIDAndReturnFalse.bind(null, CHILD),
307 );
308 await putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
309 await putListener(
310 GRANDPARENT,
311 ON_CLICK_KEY,
312 recordID.bind(null, GRANDPARENT),
313 );
314 await act(() => {
315 CHILD.click();
316 });
317 expect(idCallOrder.length).toBe(3);
318 expect(idCallOrder[0]).toBe(CHILD);
319 expect(idCallOrder[1]).toBe(PARENT);
320 expect(idCallOrder[2]).toBe(GRANDPARENT);
321 });
322
323 /**
324 * The entire event registration state of the world should be "locked-in" at
325 * the time the event occurs. This is to resolve many edge cases that come
326 * about from a listener on a lower-in-DOM node causing structural changes at
327 * places higher in the DOM. If this lower-in-DOM node causes new content to
328 * be rendered at a place higher-in-DOM, we need to be careful not to invoke
329 * these new listeners.
330 */
331
332 it('should invoke handlers that were removed while bubbling', async () => {
333 await renderTree();
334 const handleParentClick = jest.fn();
335 const handleChildClick = async function (event) {
336 await deleteAllListeners(PARENT);
337 };
338 await putListener(CHILD, ON_CLICK_KEY, handleChildClick);
339 await putListener(PARENT, ON_CLICK_KEY, handleParentClick);
340 await act(() => {
341 CHILD.click();
342 });
343 expect(handleParentClick).toHaveBeenCalledTimes(1);
344 });
345
346 it('should not invoke newly inserted handlers while bubbling', async () => {
347 await renderTree();
348 const handleParentClick = jest.fn();
349 const handleChildClick = async function (event) {
350 await putListener(PARENT, ON_CLICK_KEY, handleParentClick);
351 };
352 await putListener(CHILD, ON_CLICK_KEY, handleChildClick);
353 await act(() => {
354 CHILD.click();
355 });
356 expect(handleParentClick).toHaveBeenCalledTimes(0);
357 });
358 });