@samitouri / QOS-React-1 / commits / 2efb142606

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 }