main
js 566 lines 16 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 act;
14 let Scheduler;
15 let ReactDOMClient;
16 let simulateEventDispatch;
17 let assertLog;
18
19 describe('ReactInternalTestUtilsDOM', () => {
20 beforeEach(() => {
21 jest.resetModules();
22 act = require('internal-test-utils').act;
23 simulateEventDispatch =
24 require('internal-test-utils').simulateEventDispatch;
25 Scheduler = require('scheduler/unstable_mock');
26 ReactDOMClient = require('react-dom/client');
27 React = require('react');
28 assertLog = require('internal-test-utils').assertLog;
29 });
30
31 describe('simulateEventDispatch', () => {
32 it('should batch discrete capture events', async () => {
33 let childRef;
34 function Component() {
35 const [state, setState] = React.useState(0);
36 Scheduler.log(`Render ${state}`);
37 return (
38 <div
39 onClickCapture={() => {
40 queueMicrotask(() => {
41 Scheduler.log('Parent microtask');
42 });
43 setState(1);
44 Scheduler.log('onClickCapture parent');
45 }}>
46 <button
47 ref={ref => (childRef = ref)}
48 onClickCapture={() => {
49 queueMicrotask(() => {
50 Scheduler.log('Child microtask');
51 });
52 setState(2);
53 Scheduler.log('onClickCapture child');
54 }}
55 />
56 </div>
57 );
58 }
59
60 const container = document.createElement('div');
61 document.body.appendChild(container);
62 const root = ReactDOMClient.createRoot(container);
63 await act(() => {
64 root.render(<Component />);
65 });
66
67 assertLog(['Render 0']);
68
69 await act(async () => {
70 await simulateEventDispatch(childRef, 'click');
71 });
72
73 // Capture runs on every event we dispatch,
74 // which means we get two for the parent, and one for the child.
75 assertLog([
76 'onClickCapture parent',
77 'onClickCapture child',
78 'Parent microtask',
79 'Render 2',
80 'Child microtask',
81 ]);
82
83 document.body.removeChild(container);
84 });
85
86 it('should batch continuous capture events', async () => {
87 let childRef;
88 function Component() {
89 const [state, setState] = React.useState(0);
90 Scheduler.log(`Render ${state}`);
91 return (
92 <div
93 onMouseOutCapture={() => {
94 queueMicrotask(() => {
95 Scheduler.log('Parent microtask');
96 });
97 setState(1);
98 Scheduler.log('onMouseOutCapture parent');
99 }}>
100 <button
101 ref={ref => (childRef = ref)}
102 onMouseOutCapture={() => {
103 queueMicrotask(() => {
104 Scheduler.log('Child microtask');
105 });
106 setState(2);
107 Scheduler.log('onMouseOutCapture child');
108 }}
109 />
110 </div>
111 );
112 }
113
114 const container = document.createElement('div');
115 document.body.appendChild(container);
116 const root = ReactDOMClient.createRoot(container);
117 await act(() => {
118 root.render(<Component />);
119 });
120
121 assertLog(['Render 0']);
122
123 await act(async () => {
124 await simulateEventDispatch(childRef, 'mouseout');
125 });
126
127 assertLog([
128 'onMouseOutCapture parent',
129 'onMouseOutCapture child',
130 'Parent microtask',
131 'Child microtask',
132 'Render 2',
133 ]);
134 });
135
136 it('should batch bubbling discrete events', async () => {
137 let childRef;
138 function Component() {
139 const [state, setState] = React.useState(0);
140 Scheduler.log(`Render ${state}`);
141 return (
142 <div
143 onClick={() => {
144 queueMicrotask(() => {
145 Scheduler.log('Parent microtask');
146 });
147 setState(1);
148 Scheduler.log('onClick parent');
149 }}>
150 <button
151 ref={ref => (childRef = ref)}
152 onClick={() => {
153 queueMicrotask(() => {
154 Scheduler.log('Child microtask');
155 });
156 setState(2);
157 Scheduler.log('onClick child');
158 }}
159 />
160 </div>
161 );
162 }
163
164 const container = document.createElement('div');
165 document.body.appendChild(container);
166 const root = ReactDOMClient.createRoot(container);
167 await act(() => {
168 root.render(<Component />);
169 });
170
171 assertLog(['Render 0']);
172
173 await act(async () => {
174 await simulateEventDispatch(childRef, 'click');
175 });
176
177 assertLog([
178 'onClick child',
179 'onClick parent',
180 'Child microtask',
181 'Render 1',
182 'Parent microtask',
183 ]);
184 });
185
186 it('should batch bubbling continuous events', async () => {
187 let childRef;
188 function Component() {
189 const [state, setState] = React.useState(0);
190 Scheduler.log(`Render ${state}`);
191 return (
192 <div
193 onMouseOut={() => {
194 queueMicrotask(() => {
195 Scheduler.log('Parent microtask');
196 });
197 setState(1);
198 Scheduler.log('onMouseOut parent');
199 }}>
200 <button
201 ref={ref => (childRef = ref)}
202 onMouseOut={() => {
203 queueMicrotask(() => {
204 Scheduler.log('Child microtask');
205 });
206 setState(2);
207 Scheduler.log('onMouseOut child');
208 }}
209 />
210 </div>
211 );
212 }
213
214 const container = document.createElement('div');
215 document.body.appendChild(container);
216 const root = ReactDOMClient.createRoot(container);
217 await act(() => {
218 root.render(<Component />);
219 });
220
221 assertLog(['Render 0']);
222
223 await act(async () => {
224 await simulateEventDispatch(childRef, 'mouseout');
225 });
226
227 assertLog([
228 'onMouseOut child',
229 'onMouseOut parent',
230 'Child microtask',
231 'Parent microtask',
232 'Render 1',
233 ]);
234 });
235
236 it('does not batch discrete events between handlers', async () => {
237 let childRef = React.createRef();
238 function Component() {
239 const [state, setState] = React.useState(0);
240 const parentRef = React.useRef();
241 React.useEffect(() => {
242 function handleParentEvent() {
243 queueMicrotask(() => {
244 Scheduler.log('Parent microtask');
245 });
246 setState(2);
247 Scheduler.log(`Click parent`);
248 }
249
250 function handleChildEvent() {
251 queueMicrotask(() => {
252 Scheduler.log('Child microtask');
253 });
254 setState(1);
255 Scheduler.log(`Click child`);
256 }
257 parentRef.current.addEventListener('click', handleParentEvent);
258
259 childRef.current.addEventListener('click', handleChildEvent);
260
261 return () => {
262 parentRef.current.removeEventListener('click', handleParentEvent);
263
264 childRef.current.removeEventListener('click', handleChildEvent);
265 };
266 });
267
268 Scheduler.log(`Render ${state}`);
269 return (
270 <div ref={parentRef}>
271 <button ref={childRef} />
272 </div>
273 );
274 }
275
276 const container = document.createElement('div');
277 document.body.appendChild(container);
278 const root = ReactDOMClient.createRoot(container);
279 await act(() => {
280 root.render(<Component />);
281 });
282
283 assertLog(['Render 0']);
284
285 await act(async () => {
286 await simulateEventDispatch(childRef.current, 'click');
287 });
288
289 assertLog([
290 'Click child',
291 'Child microtask',
292 'Render 1',
293 'Click parent',
294 'Parent microtask',
295 'Render 2',
296 ]);
297 });
298
299 it('should batch continuous events between handlers', async () => {
300 let childRef = React.createRef();
301 function Component() {
302 const [state, setState] = React.useState(0);
303 const parentRef = React.useRef();
304 React.useEffect(() => {
305 function handleChildEvent() {
306 queueMicrotask(() => {
307 Scheduler.log('Child microtask');
308 });
309 setState(1);
310 Scheduler.log(`Mouseout child`);
311 }
312 function handleParentEvent() {
313 queueMicrotask(() => {
314 Scheduler.log('Parent microtask');
315 });
316 setState(2);
317 Scheduler.log(`Mouseout parent`);
318 }
319 parentRef.current.addEventListener('mouseout', handleParentEvent);
320
321 childRef.current.addEventListener('mouseout', handleChildEvent);
322
323 return () => {
324 parentRef.current.removeEventListener(
325 'mouseout',
326 handleParentEvent
327 );
328
329 childRef.current.removeEventListener('mouseout', handleChildEvent);
330 };
331 });
332
333 Scheduler.log(`Render ${state}`);
334 return (
335 <div ref={parentRef}>
336 <button ref={childRef} />
337 </div>
338 );
339 }
340
341 const container = document.createElement('div');
342 document.body.appendChild(container);
343 const root = ReactDOMClient.createRoot(container);
344 await act(() => {
345 root.render(<Component />);
346 });
347
348 assertLog(['Render 0']);
349
350 await act(async () => {
351 await simulateEventDispatch(childRef.current, 'mouseout');
352 });
353
354 assertLog([
355 'Mouseout child',
356 'Child microtask',
357 'Mouseout parent',
358 'Parent microtask',
359 'Render 2',
360 ]);
361 });
362
363 it('should flush discrete events between handlers from different roots', async () => {
364 const childContainer = document.createElement('div');
365 const parentContainer = document.createElement('main');
366
367 const childRoot = ReactDOMClient.createRoot(childContainer);
368 const parentRoot = ReactDOMClient.createRoot(parentContainer);
369 let childSetState;
370
371 function Parent() {
372 // eslint-disable-next-line no-unused-vars
373 const [state, _] = React.useState('Parent');
374 const handleClick = () => {
375 Promise.resolve().then(() => Scheduler.log('Flush Parent microtask'));
376 childSetState(2);
377 Scheduler.log('Parent click');
378 };
379 return <section onClick={handleClick}>{state}</section>;
380 }
381
382 function Child() {
383 const [state, setState] = React.useState('Child');
384 childSetState = setState;
385 const handleClick = () => {
386 Promise.resolve().then(() => Scheduler.log('Flush Child microtask'));
387 setState(1);
388 Scheduler.log('Child click');
389 };
390 Scheduler.log('Render ' + state);
391 return <span onClick={handleClick}>{state}</span>;
392 }
393
394 await act(() => {
395 childRoot.render(<Child />);
396 parentRoot.render(<Parent />);
397 });
398
399 const childNode = childContainer.firstChild;
400 const parentNode = parentContainer.firstChild;
401
402 parentNode.appendChild(childContainer);
403 document.body.appendChild(parentContainer);
404
405 assertLog(['Render Child']);
406 try {
407 await act(async () => {
408 await simulateEventDispatch(childNode, 'click');
409 });
410
411 // Since discrete events flush in a microtasks, they flush before
412 // the handler for the other root is called, after the microtask
413 // scheduled in the event fires.
414 assertLog([
415 'Child click',
416 'Flush Child microtask',
417 'Render 1',
418 'Parent click',
419 'Flush Parent microtask',
420 'Render 2',
421 ]);
422 } finally {
423 document.body.removeChild(parentContainer);
424 }
425 });
426
427 it('should batch continuous events between handlers from different roots', async () => {
428 const childContainer = document.createElement('div');
429 const parentContainer = document.createElement('main');
430
431 const childRoot = ReactDOMClient.createRoot(childContainer);
432 const parentRoot = ReactDOMClient.createRoot(parentContainer);
433 let childSetState;
434
435 function Parent() {
436 // eslint-disable-next-line no-unused-vars
437 const [state, _] = React.useState('Parent');
438 const handleMouseOut = () => {
439 Promise.resolve().then(() => Scheduler.log('Flush Parent microtask'));
440 childSetState(2);
441 Scheduler.log('Parent mouseout');
442 };
443 return <section onMouseOut={handleMouseOut}>{state}</section>;
444 }
445
446 function Child() {
447 const [state, setState] = React.useState('Child');
448 childSetState = setState;
449 const handleMouseOut = () => {
450 Promise.resolve().then(() => Scheduler.log('Flush Child microtask'));
451 setState(1);
452 Scheduler.log('Child mouseout');
453 };
454 Scheduler.log('Render ' + state);
455 return <span onMouseOut={handleMouseOut}>{state}</span>;
456 }
457
458 await act(() => {
459 childRoot.render(<Child />);
460 parentRoot.render(<Parent />);
461 });
462
463 const childNode = childContainer.firstChild;
464 const parentNode = parentContainer.firstChild;
465
466 parentNode.appendChild(childContainer);
467 document.body.appendChild(parentContainer);
468
469 assertLog(['Render Child']);
470 try {
471 await act(async () => {
472 await simulateEventDispatch(childNode, 'mouseout');
473 });
474
475 // Since continuous events flush in a macrotask, they are batched after
476 // with the handler for the other root, but the microtasks scheduled
477 // in the event handlers still fire in between.
478 assertLog([
479 'Child mouseout',
480 'Flush Child microtask',
481 'Parent mouseout',
482 'Flush Parent microtask',
483 'Render 2',
484 ]);
485 } finally {
486 document.body.removeChild(parentContainer);
487 }
488 });
489
490 it('should fire on nodes removed while dispatching', async () => {
491 let childRef;
492 function Component() {
493 const parentRef = React.useRef();
494 const middleRef = React.useRef();
495 Scheduler.log(`Render`);
496 return (
497 <div
498 ref={parentRef}
499 onClick={() => {
500 Scheduler.log('onMouseOut parent');
501 }}>
502 <div ref={middleRef}>
503 <button
504 ref={ref => (childRef = ref)}
505 onClick={() => {
506 Scheduler.log('onMouseOut child');
507 childRef.parentNode.remove();
508 }}
509 />
510 </div>
511 </div>
512 );
513 }
514
515 const container = document.createElement('div');
516 document.body.appendChild(container);
517 const root = ReactDOMClient.createRoot(container);
518 await act(() => {
519 root.render(<Component />);
520 });
521
522 assertLog(['Render']);
523
524 await act(async () => {
525 await simulateEventDispatch(childRef, 'click');
526 });
527
528 assertLog(['onMouseOut child', 'onMouseOut parent']);
529 });
530
531 it('should not fire if node is not in the document', async () => {
532 let childRef;
533 function Component() {
534 Scheduler.log(`Render`);
535 return (
536 <div
537 onMouseOut={() => {
538 Scheduler.log('onMouseOut parent');
539 }}>
540 <button
541 ref={ref => (childRef = ref)}
542 onMouseOut={() => {
543 Scheduler.log('onMouseOut child');
544 }}
545 />
546 </div>
547 );
548 }
549
550 // Do not attach root to document.
551 const root = ReactDOMClient.createRoot(document.createElement('div'));
552 await act(() => {
553 root.render(<Component />);
554 });
555
556 assertLog(['Render']);
557
558 await act(async () => {
559 await simulateEventDispatch(childRef, 'mouseout');
560 });
561
562 // No events flushed, root not in document.
563 assertLog([]);
564 });
565 });
566 });