Convert ReactDOMEventListener to createRoot (#28050)
Ricky committed
Jan 24, 2024 at 23:17 UTC
2efb1426067322cdb08304432b3ac849dee52bd9
1 file changed
+520
-418
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+520
-418
@@ -12,30 +12,37 @@
12
describe('ReactDOMEventListener', () => {
13
let React;
14
let ReactDOM;
15
+ let ReactDOMClient;
16
let ReactDOMServer;
17
+ let act;
18
19
beforeEach(() => {
20
jest.resetModules();
21
React = require('react');
22
ReactDOM = require('react-dom');
23
+ ReactDOMClient = require('react-dom/client');
24
ReactDOMServer = require('react-dom/server');
25
+ act = require('internal-test-utils').act;
26
});
27
28
describe('Propagation', () => {
25
- it('should propagate events one level down', () => {
29
+ it('should propagate events one level down', async () => {
30
const mouseOut = jest.fn();
31
const onMouseOut = event => mouseOut(event.currentTarget);
32
33
const childContainer = document.createElement('div');
34
const parentContainer = document.createElement('div');
31
- const childNode = ReactDOM.render(
32
- <div onMouseOut={onMouseOut}>Child</div>,
33
- childContainer,
34
- );
35
- const parentNode = ReactDOM.render(
36
- <div onMouseOut={onMouseOut}>div</div>,
37
- parentContainer,
38
- );
35
+
36
+ const childRoot = ReactDOMClient.createRoot(childContainer);
37
+ const parentRoot = ReactDOMClient.createRoot(parentContainer);
38
+
39
+ await act(() => {
40
+ childRoot.render(<div onMouseOut={onMouseOut}>Child</div>);
41
+ parentRoot.render(<div onMouseOut={onMouseOut}>Parent</div>);
42
+ });
43
+ const parentNode = parentContainer.firstChild;
44
+ const childNode = childContainer.firstChild;
45
+
46
parentNode.appendChild(childContainer);
47
document.body.appendChild(parentContainer);
48
@@ -44,34 +51,35 @@ describe('ReactDOMEventListener', () => {
51
nativeEvent.initEvent('mouseout', true, true);
52
childNode.dispatchEvent(nativeEvent);
53
47
- expect(mouseOut).toBeCalled();
54
expect(mouseOut).toHaveBeenCalledTimes(2);
49
- expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
50
- expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
55
+ expect(mouseOut).toHaveBeenNthCalledWith(1, childNode);
56
+ expect(mouseOut).toHaveBeenNthCalledWith(2, parentNode);
57
} finally {
58
document.body.removeChild(parentContainer);
59
}
60
});
61
56
- it('should propagate events two levels down', () => {
62
+ it('should propagate events two levels down', async () => {
63
const mouseOut = jest.fn();
64
const onMouseOut = event => mouseOut(event.currentTarget);
65
66
const childContainer = document.createElement('div');
67
const parentContainer = document.createElement('div');
68
const grandParentContainer = document.createElement('div');
63
- const childNode = ReactDOM.render(
64
- <div onMouseOut={onMouseOut}>Child</div>,
65
- childContainer,
66
- );
67
- const parentNode = ReactDOM.render(
68
- <div onMouseOut={onMouseOut}>Parent</div>,
69
- parentContainer,
70
- );
71
- const grandParentNode = ReactDOM.render(
72
- <div onMouseOut={onMouseOut}>Parent</div>,
73
- grandParentContainer,
74
- );
69
+
70
+ const childRoot = ReactDOMClient.createRoot(childContainer);
71
+ const parentRoot = ReactDOMClient.createRoot(parentContainer);
72
+ const grandParentRoot = ReactDOMClient.createRoot(grandParentContainer);
73
+
74
+ await act(() => {
75
+ childRoot.render(<div onMouseOut={onMouseOut}>Child</div>);
76
+ parentRoot.render(<div onMouseOut={onMouseOut}>Parent</div>);
77
+ grandParentRoot.render(<div onMouseOut={onMouseOut}>Grandparent</div>);
78
+ });
79
+ const childNode = childContainer.firstChild;
80
+ const parentNode = parentContainer.firstChild;
81
+ const grandParentNode = grandParentContainer.firstChild;
82
+
83
parentNode.appendChild(childContainer);
84
grandParentNode.appendChild(parentContainer);
85
@@ -82,18 +90,17 @@ describe('ReactDOMEventListener', () => {
90
nativeEvent.initEvent('mouseout', true, true);
91
childNode.dispatchEvent(nativeEvent);
92
85
- expect(mouseOut).toBeCalled();
93
expect(mouseOut).toHaveBeenCalledTimes(3);
87
- expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
88
- expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
89
- expect(mouseOut.mock.calls[2][0]).toEqual(grandParentNode);
94
+ expect(mouseOut).toHaveBeenNthCalledWith(1, childNode);
95
+ expect(mouseOut).toHaveBeenNthCalledWith(2, parentNode);
96
+ expect(mouseOut).toHaveBeenNthCalledWith(3, grandParentNode);
97
} finally {
98
document.body.removeChild(grandParentContainer);
99
}
100
});
101
102
// Regression test for https://github.com/facebook/react/issues/1105
96
- it('should not get confused by disappearing elements', () => {
103
+ it('should not get confused by disappearing elements', async () => {
104
const container = document.createElement('div');
105
document.body.appendChild(container);
106
@@ -119,12 +126,17 @@ describe('ReactDOMEventListener', () => {
126
}
127
}
128
}
122
- ReactDOM.render(<MyComponent />, container);
123
- container.firstChild.dispatchEvent(
124
- new MouseEvent('click', {
125
- bubbles: true,
126
- }),
127
- );
129
+ const root = ReactDOMClient.createRoot(container);
130
+ await act(() => {
131
+ root.render(<MyComponent />);
132
+ });
133
+ await act(() => {
134
+ container.firstChild.dispatchEvent(
135
+ new MouseEvent('click', {
136
+ bubbles: true,
137
+ }),
138
+ );
139
+ });
140
expect(container.firstChild.textContent).toBe('clicked!');
141
} finally {
142
document.body.removeChild(container);
@@ -188,22 +200,17 @@ describe('ReactDOMEventListener', () => {
200
});
201
});
202
191
- it('should not fire duplicate events for a React DOM tree', () => {
203
+ it('should not fire duplicate events for a React DOM tree', async () => {
204
const mouseOut = jest.fn();
205
const onMouseOut = event => mouseOut(event.target);
206
207
+ const innerRef = React.createRef();
208
class Wrapper extends React.Component {
196
- innerRef = React.createRef();
197
- getInner = () => {
198
- return this.innerRef.current;
199
- };
200
-
209
render() {
202
- const inner = <div ref={this.innerRef}>Inner</div>;
210
return (
211
<div>
212
<div onMouseOut={onMouseOut} id="outer">
206
- {inner}
213
+ <div ref={innerRef}>Inner</div>
214
</div>
215
</div>
216
);
@@ -211,25 +218,28 @@ describe('ReactDOMEventListener', () => {
218
}
219
220
const container = document.createElement('div');
214
- const instance = ReactDOM.render(<Wrapper />, container);
221
+ const root = ReactDOMClient.createRoot(container);
222
+ await act(() => {
223
+ root.render(<Wrapper />);
224
+ });
225
226
document.body.appendChild(container);
227
228
try {
229
const nativeEvent = document.createEvent('Event');
230
nativeEvent.initEvent('mouseout', true, true);
221
- instance.getInner().dispatchEvent(nativeEvent);
231
+ await act(() => {
232
+ innerRef.current.dispatchEvent(nativeEvent);
233
+ });
234
223
- expect(mouseOut).toBeCalled();
224
- expect(mouseOut).toHaveBeenCalledTimes(1);
225
- expect(mouseOut.mock.calls[0][0]).toEqual(instance.getInner());
235
+ expect(mouseOut).toBeCalledWith(innerRef.current);
236
} finally {
237
document.body.removeChild(container);
238
}
239
});
240
241
// Regression test for https://github.com/facebook/react/pull/12877
232
- it('should not fire form events twice', () => {
242
+ it('should not fire form events twice', async () => {
243
const container = document.createElement('div');
244
document.body.appendChild(container);
245
@@ -239,43 +249,54 @@ describe('ReactDOMEventListener', () => {
249
const handleInvalid = jest.fn();
250
const handleReset = jest.fn();
251
const handleSubmit = jest.fn();
242
- ReactDOM.render(
243
- <form ref={formRef} onReset={handleReset} onSubmit={handleSubmit}>
244
- <input ref={inputRef} onInvalid={handleInvalid} />
245
- </form>,
246
- container,
247
- );
252
249
- inputRef.current.dispatchEvent(
250
- new Event('invalid', {
251
- // https://developer.mozilla.org/en-US/docs/Web/Events/invalid
252
- bubbles: false,
253
- }),
254
- );
253
+ const root = ReactDOMClient.createRoot(container);
254
+ await act(() => {
255
+ root.render(
256
+ <form ref={formRef} onReset={handleReset} onSubmit={handleSubmit}>
257
+ <input ref={inputRef} onInvalid={handleInvalid} />
258
+ </form>,
259
+ );
260
+ });
261
+
262
+ await act(() => {
263
+ inputRef.current.dispatchEvent(
264
+ new Event('invalid', {
265
+ // https://developer.mozilla.org/en-US/docs/Web/Events/invalid
266
+ bubbles: false,
267
+ }),
268
+ );
269
+ });
270
expect(handleInvalid).toHaveBeenCalledTimes(1);
271
257
- formRef.current.dispatchEvent(
258
- new Event('reset', {
259
- // https://developer.mozilla.org/en-US/docs/Web/Events/reset
260
- bubbles: true,
261
- }),
262
- );
272
+ await act(() => {
273
+ formRef.current.dispatchEvent(
274
+ new Event('reset', {
275
+ // https://developer.mozilla.org/en-US/docs/Web/Events/reset
276
+ bubbles: true,
277
+ }),
278
+ );
279
+ });
280
expect(handleReset).toHaveBeenCalledTimes(1);
281
265
- formRef.current.dispatchEvent(
266
- new Event('submit', {
267
- // https://developer.mozilla.org/en-US/docs/Web/Events/submit
268
- bubbles: true,
269
- }),
270
- );
282
+ await act(() => {
283
+ formRef.current.dispatchEvent(
284
+ new Event('submit', {
285
+ // https://developer.mozilla.org/en-US/docs/Web/Events/submit
286
+ bubbles: true,
287
+ }),
288
+ );
289
+ });
290
expect(handleSubmit).toHaveBeenCalledTimes(1);
291
273
- formRef.current.dispatchEvent(
274
- new Event('submit', {
275
- // Might happen on older browsers.
276
- bubbles: true,
277
- }),
278
- );
292
+ await act(() => {
293
+ formRef.current.dispatchEvent(
294
+ new Event('submit', {
295
+ // Might happen on older browsers.
296
+ bubbles: true,
297
+ }),
298
+ );
299
+ });
300
expect(handleSubmit).toHaveBeenCalledTimes(2); // It already fired in this test.
301
302
document.body.removeChild(container);
@@ -284,7 +305,7 @@ describe('ReactDOMEventListener', () => {
305
// This tests an implementation detail that submit/reset events are listened to
306
// at the document level, which is necessary for event replaying to work.
307
// They bubble in all modern browsers.
287
- it('should not receive submit events if native, interim DOM handler prevents it', () => {
308
+ it('should not receive submit events if native, interim DOM handler prevents it', async () => {
309
const container = document.createElement('div');
310
document.body.appendChild(container);
311
@@ -294,30 +315,34 @@ describe('ReactDOMEventListener', () => {
315
316
const handleSubmit = jest.fn();
317
const handleReset = jest.fn();
297
- ReactDOM.render(
298
- <div ref={interimRef}>
299
- <form ref={formRef} onSubmit={handleSubmit} onReset={handleReset} />
300
- </div>,
301
- container,
302
- );
318
+ const root = ReactDOMClient.createRoot(container);
319
+ await act(() => {
320
+ root.render(
321
+ <div ref={interimRef}>
322
+ <form ref={formRef} onSubmit={handleSubmit} onReset={handleReset} />
323
+ </div>,
324
+ );
325
+ });
326
327
interimRef.current.onsubmit = nativeEvent =>
328
nativeEvent.stopPropagation();
329
interimRef.current.onreset = nativeEvent => nativeEvent.stopPropagation();
330
308
- formRef.current.dispatchEvent(
309
- new Event('submit', {
310
- // https://developer.mozilla.org/en-US/docs/Web/Events/submit
311
- bubbles: true,
312
- }),
313
- );
331
+ await act(() => {
332
+ formRef.current.dispatchEvent(
333
+ new Event('submit', {
334
+ // https://developer.mozilla.org/en-US/docs/Web/Events/submit
335
+ bubbles: true,
336
+ }),
337
+ );
338
315
- formRef.current.dispatchEvent(
316
- new Event('reset', {
317
- // https://developer.mozilla.org/en-US/docs/Web/Events/reset
318
- bubbles: true,
319
- }),
320
- );
339
+ formRef.current.dispatchEvent(
340
+ new Event('reset', {
341
+ // https://developer.mozilla.org/en-US/docs/Web/Events/reset
342
+ bubbles: true,
343
+ }),
344
+ );
345
+ });
346
347
expect(handleSubmit).not.toHaveBeenCalled();
348
expect(handleReset).not.toHaveBeenCalled();
@@ -326,7 +351,7 @@ describe('ReactDOMEventListener', () => {
351
}
352
});
353
329
- it('should dispatch loadstart only for media elements', () => {
354
+ it('should dispatch loadstart only for media elements', async () => {
355
const container = document.createElement('div');
356
document.body.appendChild(container);
357
@@ -336,35 +361,41 @@ describe('ReactDOMEventListener', () => {
361
362
const handleImgLoadStart = jest.fn();
363
const handleVideoLoadStart = jest.fn();
339
- ReactDOM.render(
340
- <div>
341
- <img ref={imgRef} onLoadStart={handleImgLoadStart} />
342
- <video ref={videoRef} onLoadStart={handleVideoLoadStart} />
343
- </div>,
344
- container,
345
- );
346
-
347
- // Note for debugging: loadstart currently doesn't fire in Chrome.
348
- // https://bugs.chromium.org/p/chromium/issues/detail?id=458851
349
- imgRef.current.dispatchEvent(
350
- new ProgressEvent('loadstart', {
351
- bubbles: false,
352
- }),
353
- );
364
+ const root = ReactDOMClient.createRoot(container);
365
+ await act(() => {
366
+ root.render(
367
+ <div>
368
+ <img ref={imgRef} onLoadStart={handleImgLoadStart} />
369
+ <video ref={videoRef} onLoadStart={handleVideoLoadStart} />
370
+ </div>,
371
+ );
372
+ });
373
+
374
+ await act(() => {
375
+ // Note for debugging: loadstart currently doesn't fire in Chrome.
376
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=458851
377
+ imgRef.current.dispatchEvent(
378
+ new ProgressEvent('loadstart', {
379
+ bubbles: false,
380
+ }),
381
+ );
382
+ });
383
expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
384
356
- videoRef.current.dispatchEvent(
357
- new ProgressEvent('loadstart', {
358
- bubbles: false,
359
- }),
360
- );
385
+ await act(() => {
386
+ videoRef.current.dispatchEvent(
387
+ new ProgressEvent('loadstart', {
388
+ bubbles: false,
389
+ }),
390
+ );
391
+ });
392
expect(handleVideoLoadStart).toHaveBeenCalledTimes(1);
393
} finally {
394
document.body.removeChild(container);
395
}
396
});
397
367
- it('should not attempt to listen to unnecessary events on the top level', () => {
398
+ it('should not attempt to listen to unnecessary events on the top level', async () => {
399
const container = document.createElement('div');
400
document.body.appendChild(container);
401
@@ -451,22 +482,25 @@ describe('ReactDOMEventListener', () => {
482
try {
483
// We expect that mounting this tree will
484
// *not* attach handlers for any top-level events.
454
- ReactDOM.render(
455
- <div onPlay={handleVideoPlayDelegated}>
456
- <video ref={videoRef} {...mediaEvents} onPlay={handleVideoPlay} />
457
- <audio {...mediaEvents}>
458
- <source {...mediaEvents} />
459
- </audio>
460
- </div>,
461
- container,
462
- );
463
-
464
- // Also verify dispatching one of them works
465
- videoRef.current.dispatchEvent(
466
- new Event('play', {
467
- bubbles: false,
468
- }),
469
- );
485
+ const root = ReactDOMClient.createRoot(container);
486
+ await act(() => {
487
+ root.render(
488
+ <div onPlay={handleVideoPlayDelegated}>
489
+ <video ref={videoRef} {...mediaEvents} onPlay={handleVideoPlay} />
490
+ <audio {...mediaEvents}>
491
+ <source {...mediaEvents} />
492
+ </audio>
493
+ </div>,
494
+ );
495
+ });
496
+ await act(() => {
497
+ // Also verify dispatching one of them works
498
+ videoRef.current.dispatchEvent(
499
+ new Event('play', {
500
+ bubbles: false,
501
+ }),
502
+ );
503
+ });
504
expect(handleVideoPlay).toHaveBeenCalledTimes(1);
505
// Unlike browsers, we delegate media events.
506
// (This doesn't make a lot of sense but it would be a breaking change not to.)
@@ -478,26 +512,28 @@ describe('ReactDOMEventListener', () => {
512
}
513
});
514
481
- it('should dispatch load for embed elements', () => {
515
+ it('should dispatch load for embed elements', async () => {
516
const container = document.createElement('div');
517
document.body.appendChild(container);
518
519
try {
520
const ref = React.createRef();
521
const handleLoad = jest.fn();
488
-
489
- ReactDOM.render(
490
- <div>
491
- <embed ref={ref} onLoad={handleLoad} />
492
- </div>,
493
- container,
494
- );
495
-
496
- ref.current.dispatchEvent(
497
- new ProgressEvent('load', {
498
- bubbles: false,
499
- }),
500
- );
522
+ const root = ReactDOMClient.createRoot(container);
523
+ await act(() => {
524
+ root.render(
525
+ <div>
526
+ <embed ref={ref} onLoad={handleLoad} />
527
+ </div>,
528
+ );
529
+ });
530
+ await act(() => {
531
+ ref.current.dispatchEvent(
532
+ new ProgressEvent('load', {
533
+ bubbles: false,
534
+ }),
535
+ );
536
+ });
537
538
expect(handleLoad).toHaveBeenCalledTimes(1);
539
} finally {
@@ -507,24 +543,29 @@ describe('ReactDOMEventListener', () => {
543
544
// Unlike browsers, we delegate media events.
545
// (This doesn't make a lot of sense but it would be a breaking change not to.)
510
- it('should delegate media events even without a direct listener', () => {
546
+ it('should delegate media events even without a direct listener', async () => {
547
const container = document.createElement('div');
548
const ref = React.createRef();
549
const handleVideoPlayDelegated = jest.fn();
550
document.body.appendChild(container);
551
try {
516
- ReactDOM.render(
517
- <div onPlay={handleVideoPlayDelegated}>
518
- {/* Intentionally no handler on the target: */}
519
- <video ref={ref} />
520
- </div>,
521
- container,
522
- );
523
- ref.current.dispatchEvent(
524
- new Event('play', {
525
- bubbles: false,
526
- }),
527
- );
552
+ const root = ReactDOMClient.createRoot(container);
553
+ await act(() => {
554
+ root.render(
555
+ <div onPlay={handleVideoPlayDelegated}>
556
+ {/* Intentionally no handler on the target: */}
557
+ <video ref={ref} />
558
+ </div>,
559
+ );
560
+ });
561
+
562
+ await act(() => {
563
+ ref.current.dispatchEvent(
564
+ new Event('play', {
565
+ bubbles: false,
566
+ }),
567
+ );
568
+ });
569
// Regression test: ensure React tree delegation still works
570
// even if the actual DOM element did not have a handler.
571
expect(handleVideoPlayDelegated).toHaveBeenCalledTimes(1);
@@ -533,30 +574,34 @@ describe('ReactDOMEventListener', () => {
574
}
575
});
576
536
- it('should delegate dialog events even without a direct listener', () => {
577
+ it('should delegate dialog events even without a direct listener', async () => {
578
const container = document.createElement('div');
579
const ref = React.createRef();
580
const onCancel = jest.fn();
581
const onClose = jest.fn();
582
document.body.appendChild(container);
583
try {
543
- ReactDOM.render(
544
- <div onCancel={onCancel} onClose={onClose}>
545
- {/* Intentionally no handler on the target: */}
546
- <dialog ref={ref} />
547
- </div>,
548
- container,
549
- );
550
- ref.current.dispatchEvent(
551
- new Event('close', {
552
- bubbles: false,
553
- }),
554
- );
555
- ref.current.dispatchEvent(
556
- new Event('cancel', {
557
- bubbles: false,
558
- }),
559
- );
584
+ const root = ReactDOMClient.createRoot(container);
585
+ await act(() => {
586
+ root.render(
587
+ <div onCancel={onCancel} onClose={onClose}>
588
+ {/* Intentionally no handler on the target: */}
589
+ <dialog ref={ref} />
590
+ </div>,
591
+ );
592
+ });
593
+ await act(() => {
594
+ ref.current.dispatchEvent(
595
+ new Event('close', {
596
+ bubbles: false,
597
+ }),
598
+ );
599
+ ref.current.dispatchEvent(
600
+ new Event('cancel', {
601
+ bubbles: false,
602
+ }),
603
+ );
604
+ });
605
// Regression test: ensure React tree delegation still works
606
// even if the actual DOM element did not have a handler.
607
expect(onCancel).toHaveBeenCalledTimes(1);
@@ -566,52 +611,60 @@ describe('ReactDOMEventListener', () => {
611
}
612
});
613
569
- it('should bubble non-native bubbling toggle events', () => {
614
+ it('should bubble non-native bubbling toggle events', async () => {
615
const container = document.createElement('div');
616
const ref = React.createRef();
617
const onToggle = jest.fn();
618
document.body.appendChild(container);
619
try {
575
- ReactDOM.render(
576
- <div onToggle={onToggle}>
577
- <details ref={ref} onToggle={onToggle} />
578
- </div>,
579
- container,
580
- );
581
- ref.current.dispatchEvent(
582
- new Event('toggle', {
583
- bubbles: false,
584
- }),
585
- );
620
+ const root = ReactDOMClient.createRoot(container);
621
+ await act(() => {
622
+ root.render(
623
+ <div onToggle={onToggle}>
624
+ <details ref={ref} onToggle={onToggle} />
625
+ </div>,
626
+ );
627
+ });
628
+ await act(() => {
629
+ ref.current.dispatchEvent(
630
+ new Event('toggle', {
631
+ bubbles: false,
632
+ }),
633
+ );
634
+ });
635
expect(onToggle).toHaveBeenCalledTimes(2);
636
} finally {
637
document.body.removeChild(container);
638
}
639
});
640
592
- it('should bubble non-native bubbling cancel/close events', () => {
641
+ it('should bubble non-native bubbling cancel/close events', async () => {
642
const container = document.createElement('div');
643
const ref = React.createRef();
644
const onCancel = jest.fn();
645
const onClose = jest.fn();
646
document.body.appendChild(container);
647
try {
599
- ReactDOM.render(
600
- <div onCancel={onCancel} onClose={onClose}>
601
- <dialog ref={ref} onCancel={onCancel} onClose={onClose} />
602
- </div>,
603
- container,
604
- );
605
- ref.current.dispatchEvent(
606
- new Event('cancel', {
607
- bubbles: false,
608
- }),
609
- );
610
- ref.current.dispatchEvent(
611
- new Event('close', {
612
- bubbles: false,
613
- }),
614
- );
648
+ const root = ReactDOMClient.createRoot(container);
649
+ await act(() => {
650
+ root.render(
651
+ <div onCancel={onCancel} onClose={onClose}>
652
+ <dialog ref={ref} onCancel={onCancel} onClose={onClose} />
653
+ </div>,
654
+ );
655
+ });
656
+ await act(() => {
657
+ ref.current.dispatchEvent(
658
+ new Event('cancel', {
659
+ bubbles: false,
660
+ }),
661
+ );
662
+ ref.current.dispatchEvent(
663
+ new Event('close', {
664
+ bubbles: false,
665
+ }),
666
+ );
667
+ });
668
expect(onCancel).toHaveBeenCalledTimes(2);
669
expect(onClose).toHaveBeenCalledTimes(2);
670
} finally {
@@ -619,53 +672,62 @@ describe('ReactDOMEventListener', () => {
672
}
673
});
674
622
- it('should bubble non-native bubbling media events events', () => {
675
+ it('should bubble non-native bubbling media events events', async () => {
676
const container = document.createElement('div');
677
const ref = React.createRef();
678
const onPlay = jest.fn();
679
document.body.appendChild(container);
680
try {
628
- ReactDOM.render(
629
- <div onPlay={onPlay}>
630
- <video ref={ref} onPlay={onPlay} />
631
- </div>,
632
- container,
633
- );
634
- ref.current.dispatchEvent(
635
- new Event('play', {
636
- bubbles: false,
637
- }),
638
- );
681
+ const root = ReactDOMClient.createRoot(container);
682
+ await act(() => {
683
+ root.render(
684
+ <div onPlay={onPlay}>
685
+ <video ref={ref} onPlay={onPlay} />
686
+ </div>,
687
+ );
688
+ });
689
+ await act(() => {
690
+ ref.current.dispatchEvent(
691
+ new Event('play', {
692
+ bubbles: false,
693
+ }),
694
+ );
695
+ });
696
expect(onPlay).toHaveBeenCalledTimes(2);
697
} finally {
698
document.body.removeChild(container);
699
}
700
});
701
645
- it('should bubble non-native bubbling invalid events', () => {
702
+ it('should bubble non-native bubbling invalid events', async () => {
703
const container = document.createElement('div');
704
const ref = React.createRef();
705
const onInvalid = jest.fn();
706
document.body.appendChild(container);
707
try {
651
- ReactDOM.render(
652
- <form onInvalid={onInvalid}>
653
- <input ref={ref} onInvalid={onInvalid} />
654
- </form>,
655
- container,
656
- );
657
- ref.current.dispatchEvent(
658
- new Event('invalid', {
659
- bubbles: false,
660
- }),
661
- );
708
+ const root = ReactDOMClient.createRoot(container);
709
+ await act(() => {
710
+ root.render(
711
+ <form onInvalid={onInvalid}>
712
+ <input ref={ref} onInvalid={onInvalid} />
713
+ </form>,
714
+ );
715
+ });
716
+
717
+ await act(() => {
718
+ ref.current.dispatchEvent(
719
+ new Event('invalid', {
720
+ bubbles: false,
721
+ }),
722
+ );
723
+ });
724
expect(onInvalid).toHaveBeenCalledTimes(2);
725
} finally {
726
document.body.removeChild(container);
727
}
728
});
729
668
- it('should handle non-bubbling capture events correctly', () => {
730
+ it('should handle non-bubbling capture events correctly', async () => {
731
const container = document.createElement('div');
732
const innerRef = React.createRef();
733
const outerRef = React.createRef();
@@ -673,30 +735,36 @@ describe('ReactDOMEventListener', () => {
735
const log = [];
736
document.body.appendChild(container);
737
try {
676
- ReactDOM.render(
677
- <div ref={outerRef} onPlayCapture={onPlayCapture}>
678
- <div onPlayCapture={onPlayCapture}>
679
- <div ref={innerRef} onPlayCapture={onPlayCapture} />
680
- </div>
681
- </div>,
682
- container,
683
- );
684
- innerRef.current.dispatchEvent(
685
- new Event('play', {
686
- bubbles: false,
687
- }),
688
- );
738
+ const root = ReactDOMClient.createRoot(container);
739
+ await act(() => {
740
+ root.render(
741
+ <div ref={outerRef} onPlayCapture={onPlayCapture}>
742
+ <div onPlayCapture={onPlayCapture}>
743
+ <div ref={innerRef} onPlayCapture={onPlayCapture} />
744
+ </div>
745
+ </div>,
746
+ );
747
+ });
748
+ await act(() => {
749
+ innerRef.current.dispatchEvent(
750
+ new Event('play', {
751
+ bubbles: false,
752
+ }),
753
+ );
754
+ });
755
expect(onPlayCapture).toHaveBeenCalledTimes(3);
756
expect(log).toEqual([
757
outerRef.current,
758
outerRef.current.firstChild,
759
innerRef.current,
760
]);
695
- outerRef.current.dispatchEvent(
696
- new Event('play', {
697
- bubbles: false,
698
- }),
699
- );
761
+ await act(() => {
762
+ outerRef.current.dispatchEvent(
763
+ new Event('play', {
764
+ bubbles: false,
765
+ }),
766
+ );
767
+ });
768
expect(onPlayCapture).toHaveBeenCalledTimes(4);
769
expect(log).toEqual([
770
outerRef.current,
@@ -712,7 +780,7 @@ describe('ReactDOMEventListener', () => {
780
// We're moving towards aligning more closely with the browser.
781
// Currently we emulate bubbling for all non-bubbling events except scroll.
782
// We may expand this list in the future, removing emulated bubbling altogether.
715
- it('should not emulate bubbling of scroll events', () => {
783
+ it('should not emulate bubbling of scroll events', async () => {
784
const container = document.createElement('div');
785
const ref = React.createRef();
786
const log = [];
@@ -730,41 +798,46 @@ describe('ReactDOMEventListener', () => {
798
);
799
document.body.appendChild(container);
800
try {
733
- ReactDOM.render(
734
- <div
735
- className="grand"
736
- onScroll={onScroll}
737
- onScrollCapture={onScrollCapture}
738
- onScrollEnd={onScrollEnd}
739
- onScrollEndCapture={onScrollEndCapture}>
801
+ const root = ReactDOMClient.createRoot(container);
802
+ await act(() => {
803
+ root.render(
804
<div
741
- className="parent"
805
+ className="grand"
806
onScroll={onScroll}
807
onScrollCapture={onScrollCapture}
808
onScrollEnd={onScrollEnd}
809
onScrollEndCapture={onScrollEndCapture}>
810
<div
747
- className="child"
811
+ className="parent"
812
onScroll={onScroll}
813
onScrollCapture={onScrollCapture}
814
onScrollEnd={onScrollEnd}
751
- onScrollEndCapture={onScrollEndCapture}
752
- ref={ref}
753
- />
754
- </div>
755
- </div>,
756
- container,
757
- );
758
- ref.current.dispatchEvent(
759
- new Event('scroll', {
760
- bubbles: false,
761
- }),
762
- );
763
- ref.current.dispatchEvent(
764
- new Event('scrollend', {
765
- bubbles: false,
766
- }),
767
- );
815
+ onScrollEndCapture={onScrollEndCapture}>
816
+ <div
817
+ className="child"
818
+ onScroll={onScroll}
819
+ onScrollCapture={onScrollCapture}
820
+ onScrollEnd={onScrollEnd}
821
+ onScrollEndCapture={onScrollEndCapture}
822
+ ref={ref}
823
+ />
824
+ </div>
825
+ </div>,
826
+ );
827
+ });
828
+
829
+ await act(() => {
830
+ ref.current.dispatchEvent(
831
+ new Event('scroll', {
832
+ bubbles: false,
833
+ }),
834
+ );
835
+ ref.current.dispatchEvent(
836
+ new Event('scrollend', {
837
+ bubbles: false,
838
+ }),
839
+ );
840
+ });
841
expect(log).toEqual([
842
['onScroll', 'capture', 'grand'],
843
['onScroll', 'capture', 'parent'],
@@ -783,7 +856,7 @@ describe('ReactDOMEventListener', () => {
856
// We're moving towards aligning more closely with the browser.
857
// Currently we emulate bubbling for all non-bubbling events except scroll.
858
// We may expand this list in the future, removing emulated bubbling altogether.
786
- it('should not emulate bubbling of scroll events (no own handler)', () => {
859
+ it('should not emulate bubbling of scroll events (no own handler)', async () => {
860
const container = document.createElement('div');
861
const ref = React.createRef();
862
const log = [];
@@ -801,35 +874,39 @@ describe('ReactDOMEventListener', () => {
874
);
875
document.body.appendChild(container);
876
try {
804
- ReactDOM.render(
805
- <div
806
- className="grand"
807
- onScroll={onScroll}
808
- onScrollCapture={onScrollCapture}
809
- onScrollEnd={onScrollEnd}
810
- onScrollEndCapture={onScrollEndCapture}>
877
+ const root = ReactDOMClient.createRoot(container);
878
+ await act(() => {
879
+ root.render(
880
<div
812
- className="parent"
881
+ className="grand"
882
onScroll={onScroll}
883
onScrollCapture={onScrollCapture}
884
onScrollEnd={onScrollEnd}
885
onScrollEndCapture={onScrollEndCapture}>
817
- {/* Intentionally no handler on the child: */}
818
- <div className="child" ref={ref} />
819
- </div>
820
- </div>,
821
- container,
822
- );
823
- ref.current.dispatchEvent(
824
- new Event('scroll', {
825
- bubbles: false,
826
- }),
827
- );
828
- ref.current.dispatchEvent(
829
- new Event('scrollend', {
830
- bubbles: false,
831
- }),
832
- );
886
+ <div
887
+ className="parent"
888
+ onScroll={onScroll}
889
+ onScrollCapture={onScrollCapture}
890
+ onScrollEnd={onScrollEnd}
891
+ onScrollEndCapture={onScrollEndCapture}>
892
+ {/* Intentionally no handler on the child: */}
893
+ <div className="child" ref={ref} />
894
+ </div>
895
+ </div>,
896
+ );
897
+ });
898
+ await act(() => {
899
+ ref.current.dispatchEvent(
900
+ new Event('scroll', {
901
+ bubbles: false,
902
+ }),
903
+ );
904
+ ref.current.dispatchEvent(
905
+ new Event('scrollend', {
906
+ bubbles: false,
907
+ }),
908
+ );
909
+ });
910
expect(log).toEqual([
911
['onScroll', 'capture', 'grand'],
912
['onScroll', 'capture', 'parent'],
@@ -841,7 +918,7 @@ describe('ReactDOMEventListener', () => {
918
}
919
});
920
844
- it('should subscribe to scroll during updates', () => {
921
+ it('should subscribe to scroll during updates', async () => {
922
const container = document.createElement('div');
923
const ref = React.createRef();
924
const log = [];
@@ -859,51 +936,57 @@ describe('ReactDOMEventListener', () => {
936
);
937
document.body.appendChild(container);
938
try {
862
- ReactDOM.render(
863
- <div>
939
+ const root = ReactDOMClient.createRoot(container);
940
+ await act(() => {
941
+ root.render(
942
<div>
865
- <div />
866
- </div>
867
- </div>,
868
- container,
869
- );
943
+ <div>
944
+ <div />
945
+ </div>
946
+ </div>,
947
+ );
948
+ });
949
871
- // Update to attach.
872
- ReactDOM.render(
873
- <div
874
- className="grand"
875
- onScroll={e => onScroll(e)}
876
- onScrollCapture={e => onScrollCapture(e)}
877
- onScrollEnd={e => onScrollEnd(e)}
878
- onScrollEndCapture={e => onScrollEndCapture(e)}>
950
+ await act(() => {
951
+ // Update to attach.
952
+ root.render(
953
<div
880
- className="parent"
954
+ className="grand"
955
onScroll={e => onScroll(e)}
956
onScrollCapture={e => onScrollCapture(e)}
957
onScrollEnd={e => onScrollEnd(e)}
958
onScrollEndCapture={e => onScrollEndCapture(e)}>
959
<div
886
- className="child"
960
+ className="parent"
961
onScroll={e => onScroll(e)}
962
onScrollCapture={e => onScrollCapture(e)}
963
onScrollEnd={e => onScrollEnd(e)}
890
- onScrollEndCapture={e => onScrollEndCapture(e)}
891
- ref={ref}
892
- />
893
- </div>
894
- </div>,
895
- container,
896
- );
897
- ref.current.dispatchEvent(
898
- new Event('scroll', {
899
- bubbles: false,
900
- }),
901
- );
902
- ref.current.dispatchEvent(
903
- new Event('scrollend', {
904
- bubbles: false,
905
- }),
906
- );
964
+ onScrollEndCapture={e => onScrollEndCapture(e)}>
965
+ <div
966
+ className="child"
967
+ onScroll={e => onScroll(e)}
968
+ onScrollCapture={e => onScrollCapture(e)}
969
+ onScrollEnd={e => onScrollEnd(e)}
970
+ onScrollEndCapture={e => onScrollEndCapture(e)}
971
+ ref={ref}
972
+ />
973
+ </div>
974
+ </div>,
975
+ );
976
+ });
977
+
978
+ await act(() => {
979
+ ref.current.dispatchEvent(
980
+ new Event('scroll', {
981
+ bubbles: false,
982
+ }),
983
+ );
984
+ ref.current.dispatchEvent(
985
+ new Event('scrollend', {
986
+ bubbles: false,
987
+ }),
988
+ );
989
+ });
990
expect(log).toEqual([
991
['onScroll', 'capture', 'grand'],
992
['onScroll', 'capture', 'parent'],
@@ -917,43 +1000,46 @@ describe('ReactDOMEventListener', () => {
1000
1001
// Update to verify deduplication.
1002
log.length = 0;
920
- ReactDOM.render(
921
- <div
922
- className="grand"
923
- // Note: these are intentionally inline functions so that
924
- // we hit the reattachment codepath instead of bailing out.
925
- onScroll={e => onScroll(e)}
926
- onScrollCapture={e => onScrollCapture(e)}
927
- onScrollEnd={e => onScrollEnd(e)}
928
- onScrollEndCapture={e => onScrollEndCapture(e)}>
1003
+ await act(() => {
1004
+ root.render(
1005
<div
930
- className="parent"
1006
+ className="grand"
1007
+ // Note: these are intentionally inline functions so that
1008
+ // we hit the reattachment codepath instead of bailing out.
1009
onScroll={e => onScroll(e)}
1010
onScrollCapture={e => onScrollCapture(e)}
1011
onScrollEnd={e => onScrollEnd(e)}
1012
onScrollEndCapture={e => onScrollEndCapture(e)}>
1013
<div
936
- className="child"
1014
+ className="parent"
1015
onScroll={e => onScroll(e)}
1016
onScrollCapture={e => onScrollCapture(e)}
1017
onScrollEnd={e => onScrollEnd(e)}
940
- onScrollEndCapture={e => onScrollEndCapture(e)}
941
- ref={ref}
942
- />
943
- </div>
944
- </div>,
945
- container,
946
- );
947
- ref.current.dispatchEvent(
948
- new Event('scroll', {
949
- bubbles: false,
950
- }),
951
- );
952
- ref.current.dispatchEvent(
953
- new Event('scrollend', {
954
- bubbles: false,
955
- }),
956
- );
1018
+ onScrollEndCapture={e => onScrollEndCapture(e)}>
1019
+ <div
1020
+ className="child"
1021
+ onScroll={e => onScroll(e)}
1022
+ onScrollCapture={e => onScrollCapture(e)}
1023
+ onScrollEnd={e => onScrollEnd(e)}
1024
+ onScrollEndCapture={e => onScrollEndCapture(e)}
1025
+ ref={ref}
1026
+ />
1027
+ </div>
1028
+ </div>,
1029
+ );
1030
+ });
1031
+ await act(() => {
1032
+ ref.current.dispatchEvent(
1033
+ new Event('scroll', {
1034
+ bubbles: false,
1035
+ }),
1036
+ );
1037
+ ref.current.dispatchEvent(
1038
+ new Event('scrollend', {
1039
+ bubbles: false,
1040
+ }),
1041
+ );
1042
+ });
1043
expect(log).toEqual([
1044
['onScroll', 'capture', 'grand'],
1045
['onScroll', 'capture', 'parent'],
@@ -967,24 +1053,27 @@ describe('ReactDOMEventListener', () => {
1053
1054
// Update to detach.
1055
log.length = 0;
970
- ReactDOM.render(
971
- <div>
1056
+ await act(() => {
1057
+ root.render(
1058
<div>
973
- <div ref={ref} />
974
- </div>
975
- </div>,
976
- container,
977
- );
978
- ref.current.dispatchEvent(
979
- new Event('scroll', {
980
- bubbles: false,
981
- }),
982
- );
983
- ref.current.dispatchEvent(
984
- new Event('scrollend', {
985
- bubbles: false,
986
- }),
987
- );
1059
+ <div>
1060
+ <div ref={ref} />
1061
+ </div>
1062
+ </div>,
1063
+ );
1064
+ });
1065
+ await act(() => {
1066
+ ref.current.dispatchEvent(
1067
+ new Event('scroll', {
1068
+ bubbles: false,
1069
+ }),
1070
+ );
1071
+ ref.current.dispatchEvent(
1072
+ new Event('scrollend', {
1073
+ bubbles: false,
1074
+ }),
1075
+ );
1076
+ });
1077
expect(log).toEqual([]);
1078
} finally {
1079
document.body.removeChild(container);
@@ -992,7 +1081,7 @@ describe('ReactDOMEventListener', () => {
1081
});
1082
1083
// Regression test.
995
- it('should subscribe to scroll during hydration', () => {
1084
+ it('should subscribe to scroll during hydration', async () => {
1085
const container = document.createElement('div');
1086
const ref = React.createRef();
1087
const log = [];
@@ -1036,17 +1125,22 @@ describe('ReactDOMEventListener', () => {
1125
document.body.appendChild(container);
1126
try {
1127
container.innerHTML = ReactDOMServer.renderToString(tree);
1039
- ReactDOM.hydrate(tree, container);
1040
- ref.current.dispatchEvent(
1041
- new Event('scroll', {
1042
- bubbles: false,
1043
- }),
1044
- );
1045
- ref.current.dispatchEvent(
1046
- new Event('scrollend', {
1047
- bubbles: false,
1048
- }),
1049
- );
1128
+ let root;
1129
+ await act(() => {
1130
+ root = ReactDOMClient.hydrateRoot(container, tree);
1131
+ });
1132
+ await act(() => {
1133
+ ref.current.dispatchEvent(
1134
+ new Event('scroll', {
1135
+ bubbles: false,
1136
+ }),
1137
+ );
1138
+ ref.current.dispatchEvent(
1139
+ new Event('scrollend', {
1140
+ bubbles: false,
1141
+ }),
1142
+ );
1143
+ });
1144
expect(log).toEqual([
1145
['onScroll', 'capture', 'grand'],
1146
['onScroll', 'capture', 'parent'],
@@ -1059,31 +1153,34 @@ describe('ReactDOMEventListener', () => {
1153
]);
1154
1155
log.length = 0;
1062
- ReactDOM.render(
1063
- <div>
1156
+ await act(() => {
1157
+ root.render(
1158
<div>
1065
- <div ref={ref} />
1066
- </div>
1067
- </div>,
1068
- container,
1069
- );
1070
- ref.current.dispatchEvent(
1071
- new Event('scroll', {
1072
- bubbles: false,
1073
- }),
1074
- );
1075
- ref.current.dispatchEvent(
1076
- new Event('scrollend', {
1077
- bubbles: false,
1078
- }),
1079
- );
1159
+ <div>
1160
+ <div ref={ref} />
1161
+ </div>
1162
+ </div>,
1163
+ );
1164
+ });
1165
+ await act(() => {
1166
+ ref.current.dispatchEvent(
1167
+ new Event('scroll', {
1168
+ bubbles: false,
1169
+ }),
1170
+ );
1171
+ ref.current.dispatchEvent(
1172
+ new Event('scrollend', {
1173
+ bubbles: false,
1174
+ }),
1175
+ );
1176
+ });
1177
expect(log).toEqual([]);
1178
} finally {
1179
document.body.removeChild(container);
1180
}
1181
});
1182
1086
- it('should not subscribe to selectionchange twice', () => {
1183
+ it('should not subscribe to selectionchange twice', async () => {
1184
const log = [];
1185
1186
const originalDocAddEventListener = document.addEventListener;
@@ -1099,8 +1196,13 @@ describe('ReactDOMEventListener', () => {
1196
}
1197
};
1198
try {
1102
- ReactDOM.render(<input />, document.createElement('div'));
1103
- ReactDOM.render(<input />, document.createElement('div'));
1199
+ const rootOne = ReactDOMClient.createRoot(document.createElement('div'));
1200
+ const rootTwo = ReactDOMClient.createRoot(document.createElement('div'));
1201
+
1202
+ await act(() => {
1203
+ rootOne.render(<input />);
1204
+ rootTwo.render(<input />);
1205
+ });
1206
} finally {
1207
document.addEventListener = originalDocAddEventListener;
1208
}