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
+});