main
js 3,014 lines 81.2 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 'use strict';
9
10 describe('ReactDOMEventListener', () => {
11 let React;
12 let OuterReactDOMClient;
13 let InnerReactDOM;
14 let InnerReactDOMClient;
15 let act;
16 let container;
17 let root;
18
19 beforeEach(() => {
20 window.TextEvent = function () {};
21 jest.resetModules();
22 jest.isolateModules(() => {
23 React = require('react');
24 act = require('internal-test-utils').act;
25 OuterReactDOMClient = require('react-dom/client');
26 });
27 jest.isolateModules(() => {
28 InnerReactDOM = require('react-dom');
29 InnerReactDOMClient = require('react-dom/client');
30 });
31 expect(OuterReactDOMClient).not.toBe(InnerReactDOMClient);
32 });
33
34 afterEach(async () => {
35 await cleanup();
36 });
37
38 async function cleanup() {
39 if (container) {
40 await act(() => {
41 root.unmount();
42 });
43 document.body.removeChild(container);
44 container = null;
45 }
46 }
47
48 async function render(tree) {
49 await cleanup();
50 container = document.createElement('div');
51 document.body.appendChild(container);
52 root = OuterReactDOMClient.createRoot(container);
53 await act(() => {
54 root.render(tree);
55 });
56 }
57
58 describe('bubbling events', () => {
59 it('onAnimationEnd', async () => {
60 await testNativeBubblingEvent({
61 type: 'div',
62 reactEvent: 'onAnimationEnd',
63 reactEventType: 'animationend',
64 nativeEvent: 'animationend',
65 dispatch(node) {
66 node.dispatchEvent(
67 new Event('animationend', {
68 bubbles: true,
69 cancelable: true,
70 }),
71 );
72 },
73 });
74 });
75
76 it('onAnimationIteration', async () => {
77 await testNativeBubblingEvent({
78 type: 'div',
79 reactEvent: 'onAnimationIteration',
80 reactEventType: 'animationiteration',
81 nativeEvent: 'animationiteration',
82 dispatch(node) {
83 node.dispatchEvent(
84 new Event('animationiteration', {
85 bubbles: true,
86 cancelable: true,
87 }),
88 );
89 },
90 });
91 });
92
93 it('onAnimationStart', async () => {
94 await testNativeBubblingEvent({
95 type: 'div',
96 reactEvent: 'onAnimationStart',
97 reactEventType: 'animationstart',
98 nativeEvent: 'animationstart',
99 dispatch(node) {
100 node.dispatchEvent(
101 new Event('animationstart', {
102 bubbles: true,
103 cancelable: true,
104 }),
105 );
106 },
107 });
108 });
109
110 it('onAuxClick', async () => {
111 await testNativeBubblingEvent({
112 type: 'div',
113 reactEvent: 'onAuxClick',
114 reactEventType: 'auxclick',
115 nativeEvent: 'auxclick',
116 dispatch(node) {
117 node.dispatchEvent(
118 new KeyboardEvent('auxclick', {
119 bubbles: true,
120 cancelable: true,
121 }),
122 );
123 },
124 });
125 });
126
127 it('onBlur', async () => {
128 await testNativeBubblingEvent({
129 type: 'input',
130 reactEvent: 'onBlur',
131 reactEventType: 'blur',
132 nativeEvent: 'focusout',
133 dispatch(node) {
134 const e = new Event('focusout', {
135 bubbles: true,
136 cancelable: true,
137 });
138 node.dispatchEvent(e);
139 },
140 });
141 });
142
143 // This test will fail in legacy mode (only used in WWW)
144 // because we emulate the React 16 behavior where
145 // the click handler is attached to the document.
146 // @gate !enableLegacyFBSupport
147 it('onClick', async () => {
148 await testNativeBubblingEvent({
149 type: 'div',
150 reactEvent: 'onClick',
151 reactEventType: 'click',
152 nativeEvent: 'click',
153 dispatch(node) {
154 node.click();
155 },
156 });
157 });
158
159 it('onContextMenu', async () => {
160 await testNativeBubblingEvent({
161 type: 'div',
162 reactEvent: 'onContextMenu',
163 reactEventType: 'contextmenu',
164 nativeEvent: 'contextmenu',
165 dispatch(node) {
166 node.dispatchEvent(
167 new MouseEvent('contextmenu', {
168 bubbles: true,
169 cancelable: true,
170 }),
171 );
172 },
173 });
174 });
175
176 it('onCopy', async () => {
177 await testNativeBubblingEvent({
178 type: 'div',
179 reactEvent: 'onCopy',
180 reactEventType: 'copy',
181 nativeEvent: 'copy',
182 dispatch(node) {
183 node.dispatchEvent(
184 new Event('copy', {
185 bubbles: true,
186 cancelable: true,
187 }),
188 );
189 },
190 });
191 });
192
193 it('onCut', async () => {
194 await testNativeBubblingEvent({
195 type: 'div',
196 reactEvent: 'onCut',
197 reactEventType: 'cut',
198 nativeEvent: 'cut',
199 dispatch(node) {
200 node.dispatchEvent(
201 new Event('cut', {
202 bubbles: true,
203 cancelable: true,
204 }),
205 );
206 },
207 });
208 });
209
210 it('onDoubleClick', async () => {
211 await testNativeBubblingEvent({
212 type: 'div',
213 reactEvent: 'onDoubleClick',
214 reactEventType: 'dblclick',
215 nativeEvent: 'dblclick',
216 dispatch(node) {
217 node.dispatchEvent(
218 new KeyboardEvent('dblclick', {
219 bubbles: true,
220 cancelable: true,
221 }),
222 );
223 },
224 });
225 });
226
227 it('onDrag', async () => {
228 await testNativeBubblingEvent({
229 type: 'div',
230 reactEvent: 'onDrag',
231 reactEventType: 'drag',
232 nativeEvent: 'drag',
233 dispatch(node) {
234 node.dispatchEvent(
235 new MouseEvent('drag', {
236 bubbles: true,
237 cancelable: true,
238 }),
239 );
240 },
241 });
242 });
243
244 it('onDragEnd', async () => {
245 await testNativeBubblingEvent({
246 type: 'div',
247 reactEvent: 'onDragEnd',
248 reactEventType: 'dragend',
249 nativeEvent: 'dragend',
250 dispatch(node) {
251 node.dispatchEvent(
252 new MouseEvent('dragend', {
253 bubbles: true,
254 cancelable: true,
255 }),
256 );
257 },
258 });
259 });
260
261 it('onDragEnter', async () => {
262 await testNativeBubblingEvent({
263 type: 'div',
264 reactEvent: 'onDragEnter',
265 reactEventType: 'dragenter',
266 nativeEvent: 'dragenter',
267 dispatch(node) {
268 node.dispatchEvent(
269 new MouseEvent('dragenter', {
270 bubbles: true,
271 cancelable: true,
272 }),
273 );
274 },
275 });
276 });
277
278 it('onDragExit', async () => {
279 await testNativeBubblingEvent({
280 type: 'div',
281 reactEvent: 'onDragExit',
282 reactEventType: 'dragexit',
283 nativeEvent: 'dragexit',
284 dispatch(node) {
285 node.dispatchEvent(
286 new MouseEvent('dragexit', {
287 bubbles: true,
288 cancelable: true,
289 }),
290 );
291 },
292 });
293 });
294
295 it('onDragLeave', async () => {
296 await testNativeBubblingEvent({
297 type: 'div',
298 reactEvent: 'onDragLeave',
299 reactEventType: 'dragleave',
300 nativeEvent: 'dragleave',
301 dispatch(node) {
302 node.dispatchEvent(
303 new MouseEvent('dragleave', {
304 bubbles: true,
305 cancelable: true,
306 }),
307 );
308 },
309 });
310 });
311
312 it('onDragOver', async () => {
313 await testNativeBubblingEvent({
314 type: 'div',
315 reactEvent: 'onDragOver',
316 reactEventType: 'dragover',
317 nativeEvent: 'dragover',
318 dispatch(node) {
319 node.dispatchEvent(
320 new MouseEvent('dragover', {
321 bubbles: true,
322 cancelable: true,
323 }),
324 );
325 },
326 });
327 });
328
329 it('onDragStart', async () => {
330 await testNativeBubblingEvent({
331 type: 'div',
332 reactEvent: 'onDragStart',
333 reactEventType: 'dragstart',
334 nativeEvent: 'dragstart',
335 dispatch(node) {
336 node.dispatchEvent(
337 new MouseEvent('dragstart', {
338 bubbles: true,
339 cancelable: true,
340 }),
341 );
342 },
343 });
344 });
345
346 it('onDrop', async () => {
347 await testNativeBubblingEvent({
348 type: 'div',
349 reactEvent: 'onDrop',
350 reactEventType: 'drop',
351 nativeEvent: 'drop',
352 dispatch(node) {
353 node.dispatchEvent(
354 new MouseEvent('drop', {
355 bubbles: true,
356 cancelable: true,
357 }),
358 );
359 },
360 });
361 });
362
363 it('onFocus', async () => {
364 await testNativeBubblingEvent({
365 type: 'input',
366 reactEvent: 'onFocus',
367 reactEventType: 'focus',
368 nativeEvent: 'focusin',
369 dispatch(node) {
370 const e = new Event('focusin', {
371 bubbles: true,
372 cancelable: true,
373 });
374 node.dispatchEvent(e);
375 },
376 });
377 });
378
379 it('onGotPointerCapture', async () => {
380 await testNativeBubblingEvent({
381 type: 'div',
382 reactEvent: 'onGotPointerCapture',
383 reactEventType: 'gotpointercapture',
384 nativeEvent: 'gotpointercapture',
385 dispatch(node) {
386 node.dispatchEvent(
387 new Event('gotpointercapture', {
388 bubbles: true,
389 cancelable: true,
390 }),
391 );
392 },
393 });
394 });
395
396 it('onKeyDown', async () => {
397 await testNativeBubblingEvent({
398 type: 'input',
399 reactEvent: 'onKeyDown',
400 reactEventType: 'keydown',
401 nativeEvent: 'keydown',
402 dispatch(node) {
403 node.dispatchEvent(
404 new KeyboardEvent('keydown', {
405 bubbles: true,
406 cancelable: true,
407 }),
408 );
409 },
410 });
411 });
412
413 it('onKeyPress', async () => {
414 await testNativeBubblingEvent({
415 type: 'input',
416 reactEvent: 'onKeyPress',
417 reactEventType: 'keypress',
418 nativeEvent: 'keypress',
419 dispatch(node) {
420 node.dispatchEvent(
421 new KeyboardEvent('keypress', {
422 keyCode: 13,
423 bubbles: true,
424 cancelable: true,
425 }),
426 );
427 },
428 });
429 });
430
431 it('onKeyUp', async () => {
432 await testNativeBubblingEvent({
433 type: 'input',
434 reactEvent: 'onKeyUp',
435 reactEventType: 'keyup',
436 nativeEvent: 'keyup',
437 dispatch(node) {
438 node.dispatchEvent(
439 new KeyboardEvent('keyup', {
440 bubbles: true,
441 cancelable: true,
442 }),
443 );
444 },
445 });
446 });
447
448 it('onLostPointerCapture', async () => {
449 await testNativeBubblingEvent({
450 type: 'div',
451 reactEvent: 'onLostPointerCapture',
452 reactEventType: 'lostpointercapture',
453 nativeEvent: 'lostpointercapture',
454 dispatch(node) {
455 node.dispatchEvent(
456 new Event('lostpointercapture', {
457 bubbles: true,
458 cancelable: true,
459 }),
460 );
461 },
462 });
463 });
464
465 it('onMouseDown', async () => {
466 await testNativeBubblingEvent({
467 type: 'div',
468 reactEvent: 'onMouseDown',
469 reactEventType: 'mousedown',
470 nativeEvent: 'mousedown',
471 dispatch(node) {
472 node.dispatchEvent(
473 new MouseEvent('mousedown', {
474 bubbles: true,
475 cancelable: true,
476 }),
477 );
478 },
479 });
480 });
481
482 it('onMouseOut', async () => {
483 await testNativeBubblingEvent({
484 type: 'div',
485 reactEvent: 'onMouseOut',
486 reactEventType: 'mouseout',
487 nativeEvent: 'mouseout',
488 dispatch(node) {
489 node.dispatchEvent(
490 new MouseEvent('mouseout', {
491 bubbles: true,
492 cancelable: true,
493 }),
494 );
495 },
496 });
497 });
498
499 it('onMouseOver', async () => {
500 await testNativeBubblingEvent({
501 type: 'div',
502 reactEvent: 'onMouseOver',
503 reactEventType: 'mouseover',
504 nativeEvent: 'mouseover',
505 dispatch(node) {
506 node.dispatchEvent(
507 new MouseEvent('mouseover', {
508 bubbles: true,
509 cancelable: true,
510 }),
511 );
512 },
513 });
514 });
515
516 it('onMouseUp', async () => {
517 await testNativeBubblingEvent({
518 type: 'div',
519 reactEvent: 'onMouseUp',
520 reactEventType: 'mouseup',
521 nativeEvent: 'mouseup',
522 dispatch(node) {
523 node.dispatchEvent(
524 new MouseEvent('mouseup', {
525 bubbles: true,
526 cancelable: true,
527 }),
528 );
529 },
530 });
531 });
532
533 it('onPaste', async () => {
534 await testNativeBubblingEvent({
535 type: 'div',
536 reactEvent: 'onPaste',
537 reactEventType: 'paste',
538 nativeEvent: 'paste',
539 dispatch(node) {
540 node.dispatchEvent(
541 new Event('paste', {
542 bubbles: true,
543 cancelable: true,
544 }),
545 );
546 },
547 });
548 });
549
550 it('onPointerCancel', async () => {
551 await testNativeBubblingEvent({
552 type: 'div',
553 reactEvent: 'onPointerCancel',
554 reactEventType: 'pointercancel',
555 nativeEvent: 'pointercancel',
556 dispatch(node) {
557 node.dispatchEvent(
558 new Event('pointercancel', {
559 bubbles: true,
560 cancelable: true,
561 }),
562 );
563 },
564 });
565 });
566
567 it('onPointerDown', async () => {
568 await testNativeBubblingEvent({
569 type: 'div',
570 reactEvent: 'onPointerDown',
571 reactEventType: 'pointerdown',
572 nativeEvent: 'pointerdown',
573 dispatch(node) {
574 node.dispatchEvent(
575 new Event('pointerdown', {
576 bubbles: true,
577 cancelable: true,
578 }),
579 );
580 },
581 });
582 });
583
584 it('onPointerMove', async () => {
585 await testNativeBubblingEvent({
586 type: 'div',
587 reactEvent: 'onPointerMove',
588 reactEventType: 'pointermove',
589 nativeEvent: 'pointermove',
590 dispatch(node) {
591 node.dispatchEvent(
592 new Event('pointermove', {
593 bubbles: true,
594 cancelable: true,
595 }),
596 );
597 },
598 });
599 });
600
601 it('onPointerOut', async () => {
602 await testNativeBubblingEvent({
603 type: 'div',
604 reactEvent: 'onPointerOut',
605 reactEventType: 'pointerout',
606 nativeEvent: 'pointerout',
607 dispatch(node) {
608 node.dispatchEvent(
609 new Event('pointerout', {
610 bubbles: true,
611 cancelable: true,
612 }),
613 );
614 },
615 });
616 });
617
618 it('onPointerOver', async () => {
619 await testNativeBubblingEvent({
620 type: 'div',
621 reactEvent: 'onPointerOver',
622 reactEventType: 'pointerover',
623 nativeEvent: 'pointerover',
624 dispatch(node) {
625 node.dispatchEvent(
626 new Event('pointerover', {
627 bubbles: true,
628 cancelable: true,
629 }),
630 );
631 },
632 });
633 });
634
635 it('onPointerUp', async () => {
636 await testNativeBubblingEvent({
637 type: 'div',
638 reactEvent: 'onPointerUp',
639 reactEventType: 'pointerup',
640 nativeEvent: 'pointerup',
641 dispatch(node) {
642 node.dispatchEvent(
643 new Event('pointerup', {
644 bubbles: true,
645 cancelable: true,
646 }),
647 );
648 },
649 });
650 });
651
652 it('onReset', async () => {
653 await testNativeBubblingEvent({
654 type: 'form',
655 reactEvent: 'onReset',
656 reactEventType: 'reset',
657 nativeEvent: 'reset',
658 dispatch(node) {
659 const e = new Event('reset', {
660 bubbles: true,
661 cancelable: true,
662 });
663 node.dispatchEvent(e);
664 },
665 });
666 });
667
668 it('onSubmit', async () => {
669 await testNativeBubblingEvent({
670 type: 'form',
671 reactEvent: 'onSubmit',
672 reactEventType: 'submit',
673 nativeEvent: 'submit',
674 dispatch(node) {
675 const e = new SubmitEvent('submit', {
676 bubbles: true,
677 cancelable: true,
678 submitter: null,
679 });
680 node.dispatchEvent(e);
681 },
682 });
683 });
684
685 it('onTouchCancel', async () => {
686 await testNativeBubblingEvent({
687 type: 'div',
688 reactEvent: 'onTouchCancel',
689 reactEventType: 'touchcancel',
690 nativeEvent: 'touchcancel',
691 dispatch(node) {
692 node.dispatchEvent(
693 new Event('touchcancel', {
694 bubbles: true,
695 cancelable: true,
696 }),
697 );
698 },
699 });
700 });
701
702 it('onTouchEnd', async () => {
703 await testNativeBubblingEvent({
704 type: 'div',
705 reactEvent: 'onTouchEnd',
706 reactEventType: 'touchend',
707 nativeEvent: 'touchend',
708 dispatch(node) {
709 node.dispatchEvent(
710 new Event('touchend', {
711 bubbles: true,
712 cancelable: true,
713 }),
714 );
715 },
716 });
717 });
718
719 it('onTouchMove', async () => {
720 await testNativeBubblingEvent({
721 type: 'div',
722 reactEvent: 'onTouchMove',
723 reactEventType: 'touchmove',
724 nativeEvent: 'touchmove',
725 dispatch(node) {
726 node.dispatchEvent(
727 new Event('touchmove', {
728 bubbles: true,
729 cancelable: true,
730 }),
731 );
732 },
733 });
734 });
735
736 it('onTouchStart', async () => {
737 await testNativeBubblingEvent({
738 type: 'div',
739 reactEvent: 'onTouchStart',
740 reactEventType: 'touchstart',
741 nativeEvent: 'touchstart',
742 dispatch(node) {
743 node.dispatchEvent(
744 new Event('touchstart', {
745 bubbles: true,
746 cancelable: true,
747 }),
748 );
749 },
750 });
751 });
752
753 it('onTransitionRun', async () => {
754 await testNativeBubblingEvent({
755 type: 'div',
756 reactEvent: 'onTransitionRun',
757 reactEventType: 'transitionrun',
758 nativeEvent: 'transitionrun',
759 dispatch(node) {
760 node.dispatchEvent(
761 new Event('transitionrun', {
762 bubbles: true,
763 cancelable: false,
764 }),
765 );
766 },
767 });
768 });
769
770 it('onTransitionStart', async () => {
771 await testNativeBubblingEvent({
772 type: 'div',
773 reactEvent: 'onTransitionStart',
774 reactEventType: 'transitionstart',
775 nativeEvent: 'transitionstart',
776 dispatch(node) {
777 node.dispatchEvent(
778 new Event('transitionstart', {
779 bubbles: true,
780 cancelable: false,
781 }),
782 );
783 },
784 });
785 });
786
787 it('onTransitionCancel', async () => {
788 await testNativeBubblingEvent({
789 type: 'div',
790 reactEvent: 'onTransitionCancel',
791 reactEventType: 'transitioncancel',
792 nativeEvent: 'transitioncancel',
793 dispatch(node) {
794 node.dispatchEvent(
795 new Event('transitioncancel', {
796 bubbles: true,
797 cancelable: false,
798 }),
799 );
800 },
801 });
802 });
803
804 it('onTransitionEnd', async () => {
805 await testNativeBubblingEvent({
806 type: 'div',
807 reactEvent: 'onTransitionEnd',
808 reactEventType: 'transitionend',
809 nativeEvent: 'transitionend',
810 dispatch(node) {
811 node.dispatchEvent(
812 new Event('transitionend', {
813 bubbles: true,
814 cancelable: false,
815 }),
816 );
817 },
818 });
819 });
820
821 it('onWheel', async () => {
822 await testNativeBubblingEvent({
823 type: 'div',
824 reactEvent: 'onWheel',
825 reactEventType: 'wheel',
826 nativeEvent: 'wheel',
827 dispatch(node) {
828 node.dispatchEvent(
829 new Event('wheel', {
830 bubbles: true,
831 cancelable: true,
832 }),
833 );
834 },
835 });
836 });
837
838 it('onFullscreenChange', async () => {
839 await testNativeBubblingEvent({
840 type: 'div',
841 reactEvent: 'onFullscreenChange',
842 reactEventType: 'fullscreenchange',
843 nativeEvent: 'fullscreenchange',
844 dispatch(node) {
845 node.dispatchEvent(
846 new Event('fullscreenchange', {
847 bubbles: true,
848 cancelable: false,
849 }),
850 );
851 },
852 });
853 });
854
855 it('onFullscreenError', async () => {
856 await testNativeBubblingEvent({
857 type: 'div',
858 reactEvent: 'onFullscreenError',
859 reactEventType: 'fullscreenerror',
860 nativeEvent: 'fullscreenerror',
861 dispatch(node) {
862 node.dispatchEvent(
863 new Event('fullscreenerror', {
864 bubbles: true,
865 cancelable: false,
866 }),
867 );
868 },
869 });
870 });
871 });
872
873 describe('non-bubbling events that bubble in React', () => {
874 it('onAbort', async () => {
875 await testEmulatedBubblingEvent({
876 type: 'video',
877 reactEvent: 'onAbort',
878 reactEventType: 'abort',
879 nativeEvent: 'abort',
880 dispatch(node) {
881 const e = new Event('abort', {
882 bubbles: false,
883 cancelable: true,
884 });
885 node.dispatchEvent(e);
886 },
887 });
888 });
889
890 it('onCancel', async () => {
891 await testEmulatedBubblingEvent({
892 type: 'dialog',
893 reactEvent: 'onCancel',
894 reactEventType: 'cancel',
895 nativeEvent: 'cancel',
896 dispatch(node) {
897 const e = new Event('cancel', {
898 bubbles: false,
899 cancelable: true,
900 });
901 node.dispatchEvent(e);
902 },
903 });
904 });
905
906 it('onCanPlay', async () => {
907 await testEmulatedBubblingEvent({
908 type: 'video',
909 reactEvent: 'onCanPlay',
910 reactEventType: 'canplay',
911 nativeEvent: 'canplay',
912 dispatch(node) {
913 const e = new Event('canplay', {
914 bubbles: false,
915 cancelable: true,
916 });
917 node.dispatchEvent(e);
918 },
919 });
920 });
921
922 it('onCanPlayThrough', async () => {
923 await testEmulatedBubblingEvent({
924 type: 'video',
925 reactEvent: 'onCanPlayThrough',
926 reactEventType: 'canplaythrough',
927 nativeEvent: 'canplaythrough',
928 dispatch(node) {
929 const e = new Event('canplaythrough', {
930 bubbles: false,
931 cancelable: true,
932 });
933 node.dispatchEvent(e);
934 },
935 });
936 });
937
938 it('onClose', async () => {
939 await testEmulatedBubblingEvent({
940 type: 'dialog',
941 reactEvent: 'onClose',
942 reactEventType: 'close',
943 nativeEvent: 'close',
944 dispatch(node) {
945 const e = new Event('close', {
946 bubbles: false,
947 cancelable: true,
948 });
949 node.dispatchEvent(e);
950 },
951 });
952 });
953
954 it('onDurationChange', async () => {
955 await testEmulatedBubblingEvent({
956 type: 'video',
957 reactEvent: 'onDurationChange',
958 reactEventType: 'durationchange',
959 nativeEvent: 'durationchange',
960 dispatch(node) {
961 const e = new Event('durationchange', {
962 bubbles: false,
963 cancelable: true,
964 });
965 node.dispatchEvent(e);
966 },
967 });
968 });
969
970 it('onEmptied', async () => {
971 await testEmulatedBubblingEvent({
972 type: 'video',
973 reactEvent: 'onEmptied',
974 reactEventType: 'emptied',
975 nativeEvent: 'emptied',
976 dispatch(node) {
977 const e = new Event('emptied', {
978 bubbles: false,
979 cancelable: true,
980 });
981 node.dispatchEvent(e);
982 },
983 });
984 });
985
986 it('onEncrypted', async () => {
987 await testEmulatedBubblingEvent({
988 type: 'video',
989 reactEvent: 'onEncrypted',
990 reactEventType: 'encrypted',
991 nativeEvent: 'encrypted',
992 dispatch(node) {
993 const e = new Event('encrypted', {
994 bubbles: false,
995 cancelable: true,
996 });
997 node.dispatchEvent(e);
998 },
999 });
1000 });
1001
1002 it('onEnded', async () => {
1003 await testEmulatedBubblingEvent({
1004 type: 'video',
1005 reactEvent: 'onEnded',
1006 reactEventType: 'ended',
1007 nativeEvent: 'ended',
1008 dispatch(node) {
1009 const e = new Event('ended', {
1010 bubbles: false,
1011 cancelable: true,
1012 });
1013 node.dispatchEvent(e);
1014 },
1015 });
1016 });
1017
1018 it('onError', async () => {
1019 await testEmulatedBubblingEvent({
1020 type: 'img',
1021 reactEvent: 'onError',
1022 reactEventType: 'error',
1023 nativeEvent: 'error',
1024 dispatch(node) {
1025 const e = new Event('error', {
1026 bubbles: false,
1027 cancelable: true,
1028 });
1029 node.dispatchEvent(e);
1030 },
1031 });
1032 });
1033
1034 it('onInvalid', async () => {
1035 await testEmulatedBubblingEvent({
1036 type: 'input',
1037 reactEvent: 'onInvalid',
1038 reactEventType: 'invalid',
1039 nativeEvent: 'invalid',
1040 dispatch(node) {
1041 const e = new Event('invalid', {
1042 bubbles: false,
1043 cancelable: true,
1044 });
1045 node.dispatchEvent(e);
1046 },
1047 });
1048 });
1049
1050 it('onLoad', async () => {
1051 await testEmulatedBubblingEvent({
1052 type: 'img',
1053 reactEvent: 'onLoad',
1054 reactEventType: 'load',
1055 nativeEvent: 'load',
1056 dispatch(node) {
1057 const e = new Event('load', {
1058 bubbles: false,
1059 cancelable: true,
1060 });
1061 node.dispatchEvent(e);
1062 },
1063 });
1064 });
1065
1066 it('onLoadedData', async () => {
1067 await testEmulatedBubblingEvent({
1068 type: 'video',
1069 reactEvent: 'onLoadedData',
1070 reactEventType: 'loadeddata',
1071 nativeEvent: 'loadeddata',
1072 dispatch(node) {
1073 const e = new Event('loadeddata', {
1074 bubbles: false,
1075 cancelable: true,
1076 });
1077 node.dispatchEvent(e);
1078 },
1079 });
1080 });
1081
1082 it('onLoadedMetadata', async () => {
1083 await testEmulatedBubblingEvent({
1084 type: 'video',
1085 reactEvent: 'onLoadedMetadata',
1086 reactEventType: 'loadedmetadata',
1087 nativeEvent: 'loadedmetadata',
1088 dispatch(node) {
1089 const e = new Event('loadedmetadata', {
1090 bubbles: false,
1091 cancelable: true,
1092 });
1093 node.dispatchEvent(e);
1094 },
1095 });
1096 });
1097
1098 it('onLoadStart', async () => {
1099 await testEmulatedBubblingEvent({
1100 type: 'video',
1101 reactEvent: 'onLoadStart',
1102 reactEventType: 'loadstart',
1103 nativeEvent: 'loadstart',
1104 dispatch(node) {
1105 const e = new Event('loadstart', {
1106 bubbles: false,
1107 cancelable: true,
1108 });
1109 node.dispatchEvent(e);
1110 },
1111 });
1112 });
1113
1114 it('onPause', async () => {
1115 await testEmulatedBubblingEvent({
1116 type: 'video',
1117 reactEvent: 'onPause',
1118 reactEventType: 'pause',
1119 nativeEvent: 'pause',
1120 dispatch(node) {
1121 const e = new Event('pause', {
1122 bubbles: false,
1123 cancelable: true,
1124 });
1125 node.dispatchEvent(e);
1126 },
1127 });
1128 });
1129
1130 it('onPlay', async () => {
1131 await testEmulatedBubblingEvent({
1132 type: 'video',
1133 reactEvent: 'onPlay',
1134 reactEventType: 'play',
1135 nativeEvent: 'play',
1136 dispatch(node) {
1137 const e = new Event('play', {
1138 bubbles: false,
1139 cancelable: true,
1140 });
1141 node.dispatchEvent(e);
1142 },
1143 });
1144 });
1145
1146 it('onPlaying', async () => {
1147 await testEmulatedBubblingEvent({
1148 type: 'video',
1149 reactEvent: 'onPlaying',
1150 reactEventType: 'playing',
1151 nativeEvent: 'playing',
1152 dispatch(node) {
1153 const e = new Event('playing', {
1154 bubbles: false,
1155 cancelable: true,
1156 });
1157 node.dispatchEvent(e);
1158 },
1159 });
1160 });
1161
1162 it('onProgress', async () => {
1163 await testEmulatedBubblingEvent({
1164 type: 'video',
1165 reactEvent: 'onProgress',
1166 reactEventType: 'progress',
1167 nativeEvent: 'progress',
1168 dispatch(node) {
1169 const e = new Event('progress', {
1170 bubbles: false,
1171 cancelable: true,
1172 });
1173 node.dispatchEvent(e);
1174 },
1175 });
1176 });
1177
1178 it('onRateChange', async () => {
1179 await testEmulatedBubblingEvent({
1180 type: 'video',
1181 reactEvent: 'onRateChange',
1182 reactEventType: 'ratechange',
1183 nativeEvent: 'ratechange',
1184 dispatch(node) {
1185 const e = new Event('ratechange', {
1186 bubbles: false,
1187 cancelable: true,
1188 });
1189 node.dispatchEvent(e);
1190 },
1191 });
1192 });
1193
1194 it('onResize', async () => {
1195 await testEmulatedBubblingEvent({
1196 type: 'video',
1197 reactEvent: 'onResize',
1198 reactEventType: 'resize',
1199 nativeEvent: 'resize',
1200 dispatch(node) {
1201 const e = new Event('resize', {
1202 bubbles: false,
1203 cancelable: true,
1204 });
1205 node.dispatchEvent(e);
1206 },
1207 });
1208 });
1209
1210 it('onSeeked', async () => {
1211 await testEmulatedBubblingEvent({
1212 type: 'video',
1213 reactEvent: 'onSeeked',
1214 reactEventType: 'seeked',
1215 nativeEvent: 'seeked',
1216 dispatch(node) {
1217 const e = new Event('seeked', {
1218 bubbles: false,
1219 cancelable: true,
1220 });
1221 node.dispatchEvent(e);
1222 },
1223 });
1224 });
1225
1226 it('onSeeking', async () => {
1227 await testEmulatedBubblingEvent({
1228 type: 'video',
1229 reactEvent: 'onSeeking',
1230 reactEventType: 'seeking',
1231 nativeEvent: 'seeking',
1232 dispatch(node) {
1233 const e = new Event('seeking', {
1234 bubbles: false,
1235 cancelable: true,
1236 });
1237 node.dispatchEvent(e);
1238 },
1239 });
1240 });
1241
1242 it('onStalled', async () => {
1243 await testEmulatedBubblingEvent({
1244 type: 'video',
1245 reactEvent: 'onStalled',
1246 reactEventType: 'stalled',
1247 nativeEvent: 'stalled',
1248 dispatch(node) {
1249 const e = new Event('stalled', {
1250 bubbles: false,
1251 cancelable: true,
1252 });
1253 node.dispatchEvent(e);
1254 },
1255 });
1256 });
1257
1258 it('onSuspend', async () => {
1259 await testEmulatedBubblingEvent({
1260 type: 'video',
1261 reactEvent: 'onSuspend',
1262 reactEventType: 'suspend',
1263 nativeEvent: 'suspend',
1264 dispatch(node) {
1265 const e = new Event('suspend', {
1266 bubbles: false,
1267 cancelable: true,
1268 });
1269 node.dispatchEvent(e);
1270 },
1271 });
1272 });
1273
1274 it('onTimeUpdate', async () => {
1275 await testEmulatedBubblingEvent({
1276 type: 'video',
1277 reactEvent: 'onTimeUpdate',
1278 reactEventType: 'timeupdate',
1279 nativeEvent: 'timeupdate',
1280 dispatch(node) {
1281 const e = new Event('timeupdate', {
1282 bubbles: false,
1283 cancelable: true,
1284 });
1285 node.dispatchEvent(e);
1286 },
1287 });
1288 });
1289
1290 it('onToggle', async () => {
1291 await testEmulatedBubblingEvent({
1292 type: 'details',
1293 reactEvent: 'onToggle',
1294 reactEventType: 'toggle',
1295 nativeEvent: 'toggle',
1296 dispatch(node) {
1297 const e = new Event('toggle', {
1298 bubbles: false,
1299 cancelable: true,
1300 });
1301 node.dispatchEvent(e);
1302 },
1303 });
1304 });
1305
1306 it('onBeforeToggle Popover API', async () => {
1307 await testEmulatedBubblingEvent({
1308 type: 'div',
1309 targetProps: {popover: 'any'},
1310 reactEvent: 'onBeforeToggle',
1311 reactEventType: 'beforetoggle',
1312 nativeEvent: 'beforetoggle',
1313 dispatch(node) {
1314 const e = new Event('beforetoggle', {
1315 bubbles: false,
1316 cancelable: true,
1317 });
1318 node.dispatchEvent(e);
1319 },
1320 });
1321 });
1322
1323 it('onToggle Popover API', async () => {
1324 await testEmulatedBubblingEvent({
1325 type: 'div',
1326 targetProps: {popover: 'any'},
1327 reactEvent: 'onToggle',
1328 reactEventType: 'toggle',
1329 nativeEvent: 'toggle',
1330 dispatch(node) {
1331 const e = new Event('toggle', {
1332 bubbles: false,
1333 cancelable: true,
1334 });
1335 node.dispatchEvent(e);
1336 },
1337 });
1338 });
1339
1340 it('onBeforeToggle Dialog API', async () => {
1341 await testEmulatedBubblingEvent({
1342 type: 'dialog',
1343 reactEvent: 'onBeforeToggle',
1344 reactEventType: 'beforetoggle',
1345 nativeEvent: 'beforetoggle',
1346 dispatch(node) {
1347 const e = new Event('beforetoggle', {
1348 bubbles: false,
1349 cancelable: true,
1350 });
1351 node.dispatchEvent(e);
1352 },
1353 });
1354 });
1355
1356 it('onToggle Dialog API', async () => {
1357 await testEmulatedBubblingEvent({
1358 type: 'dialog',
1359 reactEvent: 'onToggle',
1360 reactEventType: 'toggle',
1361 nativeEvent: 'toggle',
1362 dispatch(node) {
1363 const e = new Event('toggle', {
1364 bubbles: false,
1365 cancelable: true,
1366 });
1367 node.dispatchEvent(e);
1368 },
1369 });
1370 });
1371
1372 it('onVolumeChange', async () => {
1373 await testEmulatedBubblingEvent({
1374 type: 'video',
1375 reactEvent: 'onVolumeChange',
1376 reactEventType: 'volumechange',
1377 nativeEvent: 'volumechange',
1378 dispatch(node) {
1379 const e = new Event('volumechange', {
1380 bubbles: false,
1381 cancelable: true,
1382 });
1383 node.dispatchEvent(e);
1384 },
1385 });
1386 });
1387
1388 it('onWaiting', async () => {
1389 await testEmulatedBubblingEvent({
1390 type: 'video',
1391 reactEvent: 'onWaiting',
1392 reactEventType: 'waiting',
1393 nativeEvent: 'waiting',
1394 dispatch(node) {
1395 const e = new Event('waiting', {
1396 bubbles: false,
1397 cancelable: true,
1398 });
1399 node.dispatchEvent(e);
1400 },
1401 });
1402 });
1403 });
1404
1405 describe('non-bubbling events that do not bubble in React', () => {
1406 it('onScroll', async () => {
1407 await testNonBubblingEvent({
1408 type: 'div',
1409 reactEvent: 'onScroll',
1410 reactEventType: 'scroll',
1411 nativeEvent: 'scroll',
1412 dispatch(node) {
1413 const e = new Event('scroll', {
1414 bubbles: false,
1415 cancelable: true,
1416 });
1417 node.dispatchEvent(e);
1418 },
1419 });
1420 });
1421
1422 it('onScrollEnd', async () => {
1423 await testNonBubblingEvent({
1424 type: 'div',
1425 reactEvent: 'onScrollEnd',
1426 reactEventType: 'scrollend',
1427 nativeEvent: 'scrollend',
1428 dispatch(node) {
1429 const e = new Event('scrollend', {
1430 bubbles: false,
1431 cancelable: true,
1432 });
1433 node.dispatchEvent(e);
1434 },
1435 });
1436 });
1437 });
1438
1439 // The tests for these events are currently very limited
1440 // because they are fully synthetic, and so they don't
1441 // work very well across different roots. For now, we'll
1442 // just document the current state in these tests.
1443 describe('enter/leave events', () => {
1444 it('onMouseEnter and onMouseLeave', async () => {
1445 const log = [];
1446 const targetRef = React.createRef();
1447 await render(
1448 <Fixture
1449 type="div"
1450 targetRef={targetRef}
1451 targetProps={{
1452 onMouseEnter: e => {
1453 log.push('---- inner enter');
1454 },
1455 onMouseLeave: e => {
1456 log.push('---- inner leave');
1457 },
1458 }}
1459 parentProps={{
1460 onMouseEnter: e => {
1461 log.push('--- inner parent enter');
1462 },
1463 onMouseLeave: e => {
1464 log.push('--- inner parent leave');
1465 },
1466 }}
1467 outerProps={{
1468 onMouseEnter: e => {
1469 log.push('-- outer enter');
1470 },
1471 onMouseLeave: e => {
1472 log.push('-- outer leave');
1473 },
1474 }}
1475 outerParentProps={{
1476 onMouseEnter: e => {
1477 log.push('- outer parent enter');
1478 },
1479 onMouseLeave: e => {
1480 log.push('- outer parent leave');
1481 },
1482 }}
1483 />,
1484 );
1485 expect(log.length).toBe(0);
1486 targetRef.current.dispatchEvent(
1487 new MouseEvent('mouseover', {
1488 bubbles: true,
1489 cancelable: true,
1490 relatedTarget: null,
1491 }),
1492 );
1493 // This order isn't ideal because each root
1494 // has a separate traversal.
1495 expect(log).toEqual(unindent`
1496 --- inner parent enter
1497 ---- inner enter
1498 - outer parent enter
1499 -- outer enter
1500 `);
1501 log.length = 0;
1502 targetRef.current.dispatchEvent(
1503 new MouseEvent('mouseout', {
1504 bubbles: true,
1505 cancelable: true,
1506 relatedTarget: document.body,
1507 }),
1508 );
1509 expect(log).toEqual(unindent`
1510 ---- inner leave
1511 --- inner parent leave
1512 -- outer leave
1513 - outer parent leave
1514 `);
1515 });
1516
1517 it('onPointerEnter and onPointerLeave', async () => {
1518 const log = [];
1519 const targetRef = React.createRef();
1520 await render(
1521 <Fixture
1522 type="div"
1523 targetRef={targetRef}
1524 targetProps={{
1525 onPointerEnter: e => {
1526 log.push('---- inner enter');
1527 },
1528 onPointerLeave: e => {
1529 log.push('---- inner leave');
1530 },
1531 }}
1532 parentProps={{
1533 onPointerEnter: e => {
1534 log.push('--- inner parent enter');
1535 },
1536 onPointerLeave: e => {
1537 log.push('--- inner parent leave');
1538 },
1539 }}
1540 outerProps={{
1541 onPointerEnter: e => {
1542 log.push('-- outer enter');
1543 },
1544 onPointerLeave: e => {
1545 log.push('-- outer leave');
1546 },
1547 }}
1548 outerParentProps={{
1549 onPointerEnter: e => {
1550 log.push('- outer parent enter');
1551 },
1552 onPointerLeave: e => {
1553 log.push('- outer parent leave');
1554 },
1555 }}
1556 />,
1557 );
1558 expect(log.length).toBe(0);
1559 targetRef.current.dispatchEvent(
1560 new Event('pointerover', {
1561 bubbles: true,
1562 cancelable: true,
1563 relatedTarget: null,
1564 }),
1565 );
1566 // This order isn't ideal because each root
1567 // has a separate traversal.
1568 expect(log).toEqual(unindent`
1569 --- inner parent enter
1570 ---- inner enter
1571 - outer parent enter
1572 -- outer enter
1573 `);
1574 log.length = 0;
1575 targetRef.current.dispatchEvent(
1576 new Event('pointerout', {
1577 bubbles: true,
1578 cancelable: true,
1579 relatedTarget: document.body,
1580 }),
1581 );
1582 expect(log).toEqual(unindent`
1583 ---- inner leave
1584 --- inner parent leave
1585 -- outer leave
1586 - outer parent leave
1587 `);
1588 });
1589 });
1590
1591 const setUntrackedValue = Object.getOwnPropertyDescriptor(
1592 HTMLInputElement.prototype,
1593 'value',
1594 ).set;
1595
1596 // The tests for these events are currently very limited
1597 // because they are fully synthetic, and so they don't
1598 // work very well across different roots. For now, we'll
1599 // just document the current state in these tests.
1600 describe('polyfilled events', () => {
1601 it('onBeforeInput', async () => {
1602 const log = [];
1603 const targetRef = React.createRef();
1604 await render(
1605 <Fixture
1606 type="input"
1607 targetRef={targetRef}
1608 targetProps={{
1609 onBeforeInput: e => {
1610 log.push('---- inner');
1611 },
1612 onBeforeInputCapture: e => {
1613 log.push('---- inner capture');
1614 },
1615 }}
1616 parentProps={{
1617 onBeforeInput: e => {
1618 log.push('--- inner parent');
1619 },
1620 onBeforeInputCapture: e => {
1621 log.push('--- inner parent capture');
1622 },
1623 }}
1624 outerProps={{
1625 onBeforeInput: e => {
1626 log.push('-- outer');
1627 },
1628 onBeforeInputCapture: e => {
1629 log.push('-- outer capture');
1630 },
1631 }}
1632 outerParentProps={{
1633 onBeforeInput: e => {
1634 log.push('- outer parent');
1635 },
1636 onBeforeInputCapture: e => {
1637 expect(e.type).toBe('beforeinput');
1638 log.push('- outer parent capture');
1639 },
1640 }}
1641 />,
1642 );
1643 expect(log.length).toBe(0);
1644 const e = new Event('textInput', {
1645 bubbles: true,
1646 });
1647 e.data = 'abcd';
1648 targetRef.current.dispatchEvent(e);
1649 // Since this is a polyfilled event,
1650 // the capture and bubble phases are
1651 // emulated, and don't align between roots.
1652 expect(log).toEqual(unindent`
1653 --- inner parent capture
1654 ---- inner capture
1655 ---- inner
1656 --- inner parent
1657 - outer parent capture
1658 -- outer capture
1659 -- outer
1660 - outer parent
1661 `);
1662 });
1663
1664 it('onChange', async () => {
1665 const log = [];
1666 const targetRef = React.createRef();
1667 await render(
1668 <Fixture
1669 type="input"
1670 targetRef={targetRef}
1671 targetProps={{
1672 onChange: e => {
1673 log.push('---- inner');
1674 },
1675 onChangeCapture: e => {
1676 log.push('---- inner capture');
1677 },
1678 }}
1679 parentProps={{
1680 onChange: e => {
1681 log.push('--- inner parent');
1682 },
1683 onChangeCapture: e => {
1684 log.push('--- inner parent capture');
1685 },
1686 }}
1687 outerProps={{
1688 onChange: e => {
1689 log.push('-- outer');
1690 },
1691 onChangeCapture: e => {
1692 log.push('-- outer capture');
1693 },
1694 }}
1695 outerParentProps={{
1696 onChange: e => {
1697 log.push('- outer parent');
1698 },
1699 onChangeCapture: e => {
1700 expect(e.type).toBe('change');
1701 log.push('- outer parent capture');
1702 },
1703 }}
1704 />,
1705 );
1706 expect(log.length).toBe(0);
1707 setUntrackedValue.call(targetRef.current, 'hello');
1708 targetRef.current.dispatchEvent(
1709 new Event('input', {
1710 bubbles: true,
1711 }),
1712 );
1713 // The outer React doesn't receive the event at all
1714 // because it is not responsible for this input.
1715 expect(log).toEqual(unindent`
1716 --- inner parent capture
1717 ---- inner capture
1718 ---- inner
1719 --- inner parent
1720 `);
1721 });
1722
1723 it('onCompositionStart', async () => {
1724 const log = [];
1725 const targetRef = React.createRef();
1726 await render(
1727 <Fixture
1728 type="input"
1729 targetRef={targetRef}
1730 targetProps={{
1731 onCompositionStart: e => {
1732 log.push('---- inner');
1733 },
1734 onCompositionStartCapture: e => {
1735 log.push('---- inner capture');
1736 },
1737 }}
1738 parentProps={{
1739 onCompositionStart: e => {
1740 log.push('--- inner parent');
1741 },
1742 onCompositionStartCapture: e => {
1743 log.push('--- inner parent capture');
1744 },
1745 }}
1746 outerProps={{
1747 onCompositionStart: e => {
1748 log.push('-- outer');
1749 },
1750 onCompositionStartCapture: e => {
1751 log.push('-- outer capture');
1752 },
1753 }}
1754 outerParentProps={{
1755 onCompositionStart: e => {
1756 log.push('- outer parent');
1757 },
1758 onCompositionStartCapture: e => {
1759 expect(e.type).toBe('compositionstart');
1760 log.push('- outer parent capture');
1761 },
1762 }}
1763 />,
1764 );
1765 expect(log.length).toBe(0);
1766 const e = new Event('compositionstart', {
1767 bubbles: true,
1768 });
1769 targetRef.current.dispatchEvent(e);
1770 // Since this is a polyfilled event,
1771 // the capture and bubble phases are
1772 // emulated, and don't align between roots.
1773 expect(log).toEqual(unindent`
1774 --- inner parent capture
1775 ---- inner capture
1776 ---- inner
1777 --- inner parent
1778 - outer parent capture
1779 -- outer capture
1780 -- outer
1781 - outer parent
1782 `);
1783 });
1784
1785 it('onCompositionEnd', async () => {
1786 const log = [];
1787 const targetRef = React.createRef();
1788 await render(
1789 <Fixture
1790 type="input"
1791 targetRef={targetRef}
1792 targetProps={{
1793 onCompositionEnd: e => {
1794 log.push('---- inner');
1795 },
1796 onCompositionEndCapture: e => {
1797 log.push('---- inner capture');
1798 },
1799 }}
1800 parentProps={{
1801 onCompositionEnd: e => {
1802 log.push('--- inner parent');
1803 },
1804 onCompositionEndCapture: e => {
1805 log.push('--- inner parent capture');
1806 },
1807 }}
1808 outerProps={{
1809 onCompositionEnd: e => {
1810 log.push('-- outer');
1811 },
1812 onCompositionEndCapture: e => {
1813 log.push('-- outer capture');
1814 },
1815 }}
1816 outerParentProps={{
1817 onCompositionEnd: e => {
1818 log.push('- outer parent');
1819 },
1820 onCompositionEndCapture: e => {
1821 expect(e.type).toBe('compositionend');
1822 log.push('- outer parent capture');
1823 },
1824 }}
1825 />,
1826 );
1827 expect(log.length).toBe(0);
1828 const e = new Event('compositionend', {
1829 bubbles: true,
1830 });
1831 targetRef.current.dispatchEvent(e);
1832 // Since this is a polyfilled event,
1833 // the capture and bubble phases are
1834 // emulated, and don't align between roots.
1835 expect(log).toEqual(unindent`
1836 --- inner parent capture
1837 ---- inner capture
1838 ---- inner
1839 --- inner parent
1840 - outer parent capture
1841 -- outer capture
1842 -- outer
1843 - outer parent
1844 `);
1845 });
1846
1847 it('onCompositionUpdate', async () => {
1848 const log = [];
1849 const targetRef = React.createRef();
1850 await render(
1851 <Fixture
1852 type="input"
1853 targetRef={targetRef}
1854 targetProps={{
1855 onCompositionUpdate: e => {
1856 log.push('---- inner');
1857 },
1858 onCompositionUpdateCapture: e => {
1859 log.push('---- inner capture');
1860 },
1861 }}
1862 parentProps={{
1863 onCompositionUpdate: e => {
1864 log.push('--- inner parent');
1865 },
1866 onCompositionUpdateCapture: e => {
1867 log.push('--- inner parent capture');
1868 },
1869 }}
1870 outerProps={{
1871 onCompositionUpdate: e => {
1872 log.push('-- outer');
1873 },
1874 onCompositionUpdateCapture: e => {
1875 log.push('-- outer capture');
1876 },
1877 }}
1878 outerParentProps={{
1879 onCompositionUpdate: e => {
1880 log.push('- outer parent');
1881 },
1882 onCompositionUpdateCapture: e => {
1883 expect(e.type).toBe('compositionupdate');
1884 log.push('- outer parent capture');
1885 },
1886 }}
1887 />,
1888 );
1889 expect(log.length).toBe(0);
1890 const e = new Event('compositionupdate', {
1891 bubbles: true,
1892 });
1893 targetRef.current.dispatchEvent(e);
1894 // Since this is a polyfilled event,
1895 // the capture and bubble phases are
1896 // emulated, and don't align between roots.
1897 expect(log).toEqual(unindent`
1898 --- inner parent capture
1899 ---- inner capture
1900 ---- inner
1901 --- inner parent
1902 - outer parent capture
1903 -- outer capture
1904 -- outer
1905 - outer parent
1906 `);
1907 });
1908
1909 it('onSelect', async () => {
1910 const log = [];
1911 const targetRef = React.createRef();
1912 await render(
1913 <Fixture
1914 type="input"
1915 targetRef={targetRef}
1916 targetProps={{
1917 onSelect: e => {
1918 log.push('---- inner');
1919 },
1920 onSelectCapture: e => {
1921 log.push('---- inner capture');
1922 },
1923 }}
1924 parentProps={{
1925 onSelect: e => {
1926 log.push('--- inner parent');
1927 },
1928 onSelectCapture: e => {
1929 log.push('--- inner parent capture');
1930 },
1931 }}
1932 outerProps={{
1933 onSelect: e => {
1934 log.push('-- outer');
1935 },
1936 onSelectCapture: e => {
1937 log.push('-- outer capture');
1938 },
1939 }}
1940 outerParentProps={{
1941 onSelect: e => {
1942 log.push('- outer parent');
1943 },
1944 onSelectCapture: e => {
1945 expect(e.type).toBe('select');
1946 log.push('- outer parent capture');
1947 },
1948 }}
1949 />,
1950 );
1951 expect(log.length).toBe(0);
1952 targetRef.current.focus();
1953 targetRef.current.dispatchEvent(
1954 new Event('keydown', {
1955 bubbles: true,
1956 }),
1957 );
1958 // The outer React doesn't receive the event at all
1959 // because it is not responsible for this input.
1960 expect(log).toEqual(unindent`
1961 --- inner parent capture
1962 ---- inner capture
1963 ---- inner
1964 --- inner parent
1965 `);
1966 });
1967 });
1968
1969 // Events that bubble in React and in the browser.
1970 // React delegates them to the root.
1971 async function testNativeBubblingEvent(config) {
1972 await testNativeBubblingEventWithTargetListener(config);
1973 await testNativeBubblingEventWithoutTargetListener(config);
1974 await testReactStopPropagationInOuterCapturePhase(config);
1975 await testReactStopPropagationInInnerCapturePhase(config);
1976 await testReactStopPropagationInInnerBubblePhase(config);
1977 await testReactStopPropagationInOuterBubblePhase(config);
1978 await testNativeStopPropagationInOuterCapturePhase(config);
1979 await testNativeStopPropagationInInnerCapturePhase(config);
1980 await testNativeStopPropagationInInnerBubblePhase(config);
1981 await testNativeStopPropagationInOuterBubblePhase(config);
1982 }
1983
1984 // Events that bubble in React but not in the browser.
1985 // React attaches them to the elements.
1986 async function testEmulatedBubblingEvent(config) {
1987 await testEmulatedBubblingEventWithTargetListener(config);
1988 await testEmulatedBubblingEventWithoutTargetListener(config);
1989 await testReactStopPropagationInOuterCapturePhase(config);
1990 await testReactStopPropagationInInnerCapturePhase(config);
1991 await testReactStopPropagationInInnerBubblePhase(config);
1992 await testNativeStopPropagationInOuterCapturePhase(config);
1993 await testNativeStopPropagationInInnerCapturePhase(config);
1994 await testNativeStopPropagationInInnerEmulatedBubblePhase(config);
1995 }
1996
1997 // Events that don't bubble either in React or in the browser.
1998 async function testNonBubblingEvent(config) {
1999 await testNonBubblingEventWithTargetListener(config);
2000 await testNonBubblingEventWithoutTargetListener(config);
2001 await testReactStopPropagationInOuterCapturePhase(config);
2002 await testReactStopPropagationInInnerCapturePhase(config);
2003 await testReactStopPropagationInInnerBubblePhase(config);
2004 await testNativeStopPropagationInOuterCapturePhase(config);
2005 await testNativeStopPropagationInInnerCapturePhase(config);
2006 }
2007
2008 async function testNativeBubblingEventWithTargetListener(eventConfig) {
2009 const log = [];
2010 const targetRef = React.createRef();
2011 await render(
2012 <Fixture
2013 type={eventConfig.type}
2014 targetRef={targetRef}
2015 targetProps={{
2016 [eventConfig.reactEvent]: e => {
2017 log.push('---- inner');
2018 },
2019 [eventConfig.reactEvent + 'Capture']: e => {
2020 log.push('---- inner capture');
2021 },
2022 }}
2023 parentProps={{
2024 [eventConfig.reactEvent]: e => {
2025 log.push('--- inner parent');
2026 },
2027 [eventConfig.reactEvent + 'Capture']: e => {
2028 log.push('--- inner parent capture');
2029 },
2030 }}
2031 outerProps={{
2032 [eventConfig.reactEvent]: e => {
2033 log.push('-- outer');
2034 },
2035 [eventConfig.reactEvent + 'Capture']: e => {
2036 log.push('-- outer capture');
2037 },
2038 }}
2039 outerParentProps={{
2040 [eventConfig.reactEvent]: e => {
2041 log.push('- outer parent');
2042 },
2043 [eventConfig.reactEvent + 'Capture']: e => {
2044 expect(e.type).toBe(eventConfig.reactEventType);
2045 log.push('- outer parent capture');
2046 },
2047 }}
2048 />,
2049 );
2050 expect(log.length).toBe(0);
2051 eventConfig.dispatch(targetRef.current);
2052 // Should print all listeners.
2053 expect(log).toEqual(unindent`
2054 - outer parent capture
2055 -- outer capture
2056 --- inner parent capture
2057 ---- inner capture
2058 ---- inner
2059 --- inner parent
2060 -- outer
2061 - outer parent
2062 `);
2063 }
2064
2065 async function testEmulatedBubblingEventWithTargetListener(eventConfig) {
2066 const log = [];
2067 const targetRef = React.createRef();
2068 await render(
2069 <Fixture
2070 type={eventConfig.type}
2071 targetRef={targetRef}
2072 targetProps={{
2073 ...eventConfig.targetProps,
2074 [eventConfig.reactEvent]: e => {
2075 log.push('---- inner');
2076 },
2077 [eventConfig.reactEvent + 'Capture']: e => {
2078 log.push('---- inner capture');
2079 },
2080 }}
2081 parentProps={{
2082 [eventConfig.reactEvent]: e => {
2083 log.push('--- inner parent');
2084 },
2085 [eventConfig.reactEvent + 'Capture']: e => {
2086 log.push('--- inner parent capture');
2087 },
2088 }}
2089 outerProps={{
2090 [eventConfig.reactEvent]: e => {
2091 log.push('-- outer');
2092 },
2093 [eventConfig.reactEvent + 'Capture']: e => {
2094 log.push('-- outer capture');
2095 },
2096 }}
2097 outerParentProps={{
2098 [eventConfig.reactEvent]: e => {
2099 log.push('- outer parent');
2100 },
2101 [eventConfig.reactEvent + 'Capture']: e => {
2102 expect(e.type).toBe(eventConfig.reactEventType);
2103 log.push('- outer parent capture');
2104 },
2105 }}
2106 />,
2107 );
2108 expect(log.length).toBe(0);
2109 eventConfig.dispatch(targetRef.current);
2110 // This event doesn't bubble natively, but React emulates it.
2111 // Since the element is created by the inner React, the bubbling
2112 // stops at the inner parent and never reaches the outer React.
2113 // In the future, we might consider not bubbling these events
2114 // at all, in which case inner parent also wouldn't be logged.
2115 expect(log).toEqual(unindent`
2116 - outer parent capture
2117 -- outer capture
2118 --- inner parent capture
2119 ---- inner capture
2120 ---- inner
2121 --- inner parent
2122 `);
2123 }
2124
2125 async function testNonBubblingEventWithTargetListener(eventConfig) {
2126 const log = [];
2127 const targetRef = React.createRef();
2128 await render(
2129 <Fixture
2130 type={eventConfig.type}
2131 targetRef={targetRef}
2132 targetProps={{
2133 [eventConfig.reactEvent]: e => {
2134 log.push('---- inner');
2135 },
2136 [eventConfig.reactEvent + 'Capture']: e => {
2137 log.push('---- inner capture');
2138 },
2139 }}
2140 parentProps={{
2141 [eventConfig.reactEvent]: e => {
2142 log.push('--- inner parent');
2143 },
2144 [eventConfig.reactEvent + 'Capture']: e => {
2145 log.push('--- inner parent capture');
2146 },
2147 }}
2148 outerProps={{
2149 [eventConfig.reactEvent]: e => {
2150 log.push('-- outer');
2151 },
2152 [eventConfig.reactEvent + 'Capture']: e => {
2153 log.push('-- outer capture');
2154 },
2155 }}
2156 outerParentProps={{
2157 [eventConfig.reactEvent]: e => {
2158 log.push('- outer parent');
2159 },
2160 [eventConfig.reactEvent + 'Capture']: e => {
2161 expect(e.type).toBe(eventConfig.reactEventType);
2162 log.push('- outer parent capture');
2163 },
2164 }}
2165 />,
2166 );
2167 expect(log.length).toBe(0);
2168 eventConfig.dispatch(targetRef.current);
2169 // This event doesn't bubble natively, and React is
2170 // not emulating it either. So it only reaches the
2171 // target and stops there.
2172 expect(log).toEqual(unindent`
2173 - outer parent capture
2174 -- outer capture
2175 --- inner parent capture
2176 ---- inner capture
2177 ---- inner
2178 `);
2179 }
2180
2181 async function testNativeBubblingEventWithoutTargetListener(eventConfig) {
2182 const log = [];
2183 const targetRef = React.createRef();
2184 await render(
2185 <Fixture
2186 type={eventConfig.type}
2187 targetRef={targetRef}
2188 targetProps={
2189 {
2190 // No listener on the target itself.
2191 }
2192 }
2193 parentProps={{
2194 [eventConfig.reactEvent]: e => {
2195 log.push('--- inner parent');
2196 },
2197 [eventConfig.reactEvent + 'Capture']: e => {
2198 log.push('--- inner parent capture');
2199 },
2200 }}
2201 outerProps={{
2202 [eventConfig.reactEvent]: e => {
2203 log.push('-- outer');
2204 },
2205 [eventConfig.reactEvent + 'Capture']: e => {
2206 log.push('-- outer capture');
2207 },
2208 }}
2209 outerParentProps={{
2210 [eventConfig.reactEvent]: e => {
2211 log.push('- outer parent');
2212 },
2213 [eventConfig.reactEvent + 'Capture']: e => {
2214 expect(e.type).toBe(eventConfig.reactEventType);
2215 log.push('- outer parent capture');
2216 },
2217 }}
2218 />,
2219 );
2220 expect(log.length).toBe(0);
2221 eventConfig.dispatch(targetRef.current);
2222 // Should print all listeners except the innermost one.
2223 expect(log).toEqual(unindent`
2224 - outer parent capture
2225 -- outer capture
2226 --- inner parent capture
2227 --- inner parent
2228 -- outer
2229 - outer parent
2230 `);
2231 }
2232
2233 async function testEmulatedBubblingEventWithoutTargetListener(eventConfig) {
2234 const log = [];
2235 const targetRef = React.createRef();
2236 await render(
2237 <Fixture
2238 type={eventConfig.type}
2239 targetRef={targetRef}
2240 targetProps={{
2241 ...eventConfig.targetProps,
2242 // No listener on the target itself.
2243 }}
2244 parentProps={{
2245 [eventConfig.reactEvent]: e => {
2246 log.push('--- inner parent');
2247 },
2248 [eventConfig.reactEvent + 'Capture']: e => {
2249 log.push('--- inner parent capture');
2250 },
2251 }}
2252 outerProps={{
2253 [eventConfig.reactEvent]: e => {
2254 log.push('-- outer');
2255 },
2256 [eventConfig.reactEvent + 'Capture']: e => {
2257 log.push('-- outer capture');
2258 },
2259 }}
2260 outerParentProps={{
2261 [eventConfig.reactEvent]: e => {
2262 log.push('- outer parent');
2263 },
2264 [eventConfig.reactEvent + 'Capture']: e => {
2265 expect(e.type).toBe(eventConfig.reactEventType);
2266 log.push('- outer parent capture');
2267 },
2268 }}
2269 />,
2270 );
2271 expect(log.length).toBe(0);
2272 eventConfig.dispatch(targetRef.current);
2273 // This event doesn't bubble natively, but React emulates it.
2274 // Since the element is created by the inner React, the bubbling
2275 // stops at the inner parent and never reaches the outer React.
2276 // In the future, we might consider not bubbling these events
2277 // at all, in which case inner parent also wouldn't be logged.
2278 expect(log).toEqual(unindent`
2279 - outer parent capture
2280 -- outer capture
2281 --- inner parent capture
2282 --- inner parent
2283 `);
2284 }
2285
2286 async function testNonBubblingEventWithoutTargetListener(eventConfig) {
2287 const log = [];
2288 const targetRef = React.createRef();
2289 await render(
2290 <Fixture
2291 type={eventConfig.type}
2292 targetRef={targetRef}
2293 targetProps={
2294 {
2295 // No listener on the target itself.
2296 }
2297 }
2298 parentProps={{
2299 [eventConfig.reactEvent]: e => {
2300 log.push('--- inner parent');
2301 },
2302 [eventConfig.reactEvent + 'Capture']: e => {
2303 log.push('--- inner parent capture');
2304 },
2305 }}
2306 outerProps={{
2307 [eventConfig.reactEvent]: e => {
2308 log.push('-- outer');
2309 },
2310 [eventConfig.reactEvent + 'Capture']: e => {
2311 log.push('-- outer capture');
2312 },
2313 }}
2314 outerParentProps={{
2315 [eventConfig.reactEvent]: e => {
2316 log.push('- outer parent');
2317 },
2318 [eventConfig.reactEvent + 'Capture']: e => {
2319 expect(e.type).toBe(eventConfig.reactEventType);
2320 log.push('- outer parent capture');
2321 },
2322 }}
2323 />,
2324 );
2325 expect(log.length).toBe(0);
2326 eventConfig.dispatch(targetRef.current);
2327 // This event doesn't bubble native, and React doesn't
2328 // emulate bubbling either. Since we don't have a target
2329 // listener, only capture phase listeners fire.
2330 expect(log).toEqual(unindent`
2331 - outer parent capture
2332 -- outer capture
2333 --- inner parent capture
2334 `);
2335 }
2336
2337 async function testReactStopPropagationInOuterCapturePhase(eventConfig) {
2338 const log = [];
2339 const targetRef = React.createRef();
2340 await render(
2341 <Fixture
2342 type={eventConfig.type}
2343 targetRef={node => {
2344 targetRef.current = node;
2345 if (node) {
2346 // No cleanup, assume we render once.
2347 node.addEventListener(eventConfig.nativeEvent, e => {
2348 // We *don't* expect this to appear in the log
2349 // at all because the event is stopped earlier.
2350 log.push('---- inner (native)');
2351 });
2352 }
2353 }}
2354 targetProps={{
2355 [eventConfig.reactEvent]: e => {
2356 log.push('---- inner');
2357 },
2358 [eventConfig.reactEvent + 'Capture']: e => {
2359 log.push('---- inner capture');
2360 },
2361 }}
2362 parentProps={{
2363 [eventConfig.reactEvent]: e => {
2364 log.push('--- inner parent');
2365 },
2366 [eventConfig.reactEvent + 'Capture']: e => {
2367 log.push('--- inner parent capture');
2368 },
2369 }}
2370 outerProps={{
2371 [eventConfig.reactEvent]: e => {
2372 log.push('-- outer');
2373 },
2374 [eventConfig.reactEvent + 'Capture']: e => {
2375 e.stopPropagation(); // <---------
2376 log.push('-- outer capture');
2377 },
2378 }}
2379 outerParentProps={{
2380 [eventConfig.reactEvent]: e => {
2381 log.push('- outer parent');
2382 },
2383 [eventConfig.reactEvent + 'Capture']: e => {
2384 expect(e.type).toBe(eventConfig.reactEventType);
2385 log.push('- outer parent capture');
2386 },
2387 }}
2388 />,
2389 );
2390 expect(log.length).toBe(0);
2391 eventConfig.dispatch(targetRef.current);
2392 // Should stop at the outer capture.
2393 // We don't get to the inner root at all.
2394 expect(log).toEqual(unindent`
2395 - outer parent capture
2396 -- outer capture
2397 `);
2398 }
2399
2400 async function testReactStopPropagationInInnerCapturePhase(eventConfig) {
2401 const log = [];
2402 const targetRef = React.createRef();
2403 await render(
2404 <Fixture
2405 type={eventConfig.type}
2406 targetRef={node => {
2407 targetRef.current = node;
2408 if (node) {
2409 // No cleanup, assume we render once.
2410 node.addEventListener(eventConfig.nativeEvent, e => {
2411 // We *don't* expect this to appear in the log
2412 // at all because the event is stopped earlier.
2413 log.push('---- inner (native)');
2414 });
2415 }
2416 }}
2417 targetProps={{
2418 [eventConfig.reactEvent]: e => {
2419 log.push('---- inner');
2420 },
2421 [eventConfig.reactEvent + 'Capture']: e => {
2422 log.push('---- inner capture');
2423 },
2424 }}
2425 parentProps={{
2426 [eventConfig.reactEvent]: e => {
2427 log.push('--- inner parent');
2428 },
2429 [eventConfig.reactEvent + 'Capture']: e => {
2430 e.stopPropagation(); // <---------
2431 log.push('--- inner parent capture');
2432 },
2433 }}
2434 outerProps={{
2435 [eventConfig.reactEvent]: e => {
2436 log.push('-- outer');
2437 },
2438 [eventConfig.reactEvent + 'Capture']: e => {
2439 log.push('-- outer capture');
2440 },
2441 }}
2442 outerParentProps={{
2443 [eventConfig.reactEvent]: e => {
2444 log.push('- outer parent');
2445 },
2446 [eventConfig.reactEvent + 'Capture']: e => {
2447 expect(e.type).toBe(eventConfig.reactEventType);
2448 log.push('- outer parent capture');
2449 },
2450 }}
2451 />,
2452 );
2453 expect(log.length).toBe(0);
2454 eventConfig.dispatch(targetRef.current);
2455 // We get to the inner root, but we don't
2456 // get to the target and we don't bubble.
2457 expect(log).toEqual(unindent`
2458 - outer parent capture
2459 -- outer capture
2460 --- inner parent capture
2461 `);
2462 }
2463
2464 async function testReactStopPropagationInInnerBubblePhase(eventConfig) {
2465 const log = [];
2466 const targetRef = React.createRef();
2467 await render(
2468 <Fixture
2469 type={eventConfig.type}
2470 targetRef={targetRef}
2471 targetProps={{
2472 ...eventConfig.targetProps,
2473 [eventConfig.reactEvent]: e => {
2474 e.stopPropagation(); // <---------
2475 log.push('---- inner');
2476 },
2477 [eventConfig.reactEvent + 'Capture']: e => {
2478 log.push('---- inner capture');
2479 },
2480 }}
2481 parentProps={{
2482 [eventConfig.reactEvent]: e => {
2483 log.push('--- inner parent');
2484 },
2485 [eventConfig.reactEvent + 'Capture']: e => {
2486 log.push('--- inner parent capture');
2487 },
2488 }}
2489 outerRef={node => {
2490 if (node) {
2491 // No cleanup, assume we render once.
2492 node.addEventListener(eventConfig.nativeEvent, e => {
2493 // We *don't* expect this to appear in the log
2494 // at all because the event is stopped earlier.
2495 log.push('-- outer (native)');
2496 });
2497 }
2498 }}
2499 outerProps={{
2500 [eventConfig.reactEvent]: e => {
2501 log.push('-- outer');
2502 },
2503 [eventConfig.reactEvent + 'Capture']: e => {
2504 log.push('-- outer capture');
2505 },
2506 }}
2507 outerParentProps={{
2508 [eventConfig.reactEvent]: e => {
2509 log.push('- outer parent');
2510 },
2511 [eventConfig.reactEvent + 'Capture']: e => {
2512 expect(e.type).toBe(eventConfig.reactEventType);
2513 log.push('- outer parent capture');
2514 },
2515 }}
2516 />,
2517 );
2518 expect(log.length).toBe(0);
2519 eventConfig.dispatch(targetRef.current);
2520 // Should stop at the target and not go further.
2521 expect(log).toEqual(unindent`
2522 - outer parent capture
2523 -- outer capture
2524 --- inner parent capture
2525 ---- inner capture
2526 ---- inner
2527 `);
2528 }
2529
2530 async function testReactStopPropagationInOuterBubblePhase(eventConfig) {
2531 const log = [];
2532 const targetRef = React.createRef();
2533 await render(
2534 <Fixture
2535 type={eventConfig.type}
2536 targetRef={targetRef}
2537 targetProps={{
2538 [eventConfig.reactEvent]: e => {
2539 log.push('---- inner');
2540 },
2541 [eventConfig.reactEvent + 'Capture']: e => {
2542 log.push('---- inner capture');
2543 },
2544 }}
2545 parentProps={{
2546 [eventConfig.reactEvent]: e => {
2547 log.push('--- inner parent');
2548 },
2549 [eventConfig.reactEvent + 'Capture']: e => {
2550 log.push('--- inner parent capture');
2551 },
2552 }}
2553 outerProps={{
2554 [eventConfig.reactEvent]: e => {
2555 e.stopPropagation(); // <---------
2556 log.push('-- outer');
2557 },
2558 [eventConfig.reactEvent + 'Capture']: e => {
2559 log.push('-- outer capture');
2560 },
2561 }}
2562 outerParentProps={{
2563 [eventConfig.reactEvent]: e => {
2564 log.push('- outer parent');
2565 },
2566 [eventConfig.reactEvent + 'Capture']: e => {
2567 expect(e.type).toBe(eventConfig.reactEventType);
2568 log.push('- outer parent capture');
2569 },
2570 }}
2571 />,
2572 );
2573 expect(log.length).toBe(0);
2574 eventConfig.dispatch(targetRef.current);
2575 // Should not reach the parent outer bubble handler.
2576 expect(log).toEqual(unindent`
2577 - outer parent capture
2578 -- outer capture
2579 --- inner parent capture
2580 ---- inner capture
2581 ---- inner
2582 --- inner parent
2583 -- outer
2584 `);
2585 }
2586
2587 async function testNativeStopPropagationInOuterCapturePhase(eventConfig) {
2588 const log = [];
2589 const targetRef = React.createRef();
2590 await render(
2591 <Fixture
2592 type={eventConfig.type}
2593 targetRef={targetRef}
2594 targetProps={{
2595 [eventConfig.reactEvent]: e => {
2596 log.push('---- inner');
2597 },
2598 [eventConfig.reactEvent + 'Capture']: e => {
2599 log.push('---- inner capture');
2600 },
2601 }}
2602 parentProps={{
2603 [eventConfig.reactEvent]: e => {
2604 log.push('--- inner parent');
2605 },
2606 [eventConfig.reactEvent + 'Capture']: e => {
2607 log.push('--- inner parent capture');
2608 },
2609 }}
2610 outerProps={{
2611 [eventConfig.reactEvent]: e => {
2612 log.push('-- outer');
2613 },
2614 [eventConfig.reactEvent + 'Capture']: e => {
2615 log.push('-- outer capture');
2616 },
2617 }}
2618 outerParentRef={node => {
2619 if (node) {
2620 // No cleanup, assume we render once.
2621 node.addEventListener(
2622 eventConfig.nativeEvent,
2623 e => {
2624 log.push('- outer parent capture (native)');
2625 e.stopPropagation(); // <---------
2626 },
2627 {capture: true},
2628 );
2629 }
2630 }}
2631 outerParentProps={{
2632 [eventConfig.reactEvent]: e => {
2633 log.push('- outer parent');
2634 },
2635 [eventConfig.reactEvent + 'Capture']: e => {
2636 expect(e.type).toBe(eventConfig.reactEventType);
2637 log.push('- outer parent capture');
2638 },
2639 }}
2640 />,
2641 );
2642 expect(log.length).toBe(0);
2643 eventConfig.dispatch(targetRef.current);
2644 // The outer root has already received the event,
2645 // so the capture phrase runs for it. But the inner
2646 // root is prevented from receiving it by the native
2647 // handler in the outer native capture phase.
2648 expect(log).toEqual(unindent`
2649 - outer parent capture
2650 -- outer capture
2651 - outer parent capture (native)
2652 `);
2653 }
2654
2655 async function testNativeStopPropagationInInnerCapturePhase(eventConfig) {
2656 const log = [];
2657 const targetRef = React.createRef();
2658 await render(
2659 <Fixture
2660 type={eventConfig.type}
2661 targetRef={targetRef}
2662 targetProps={{
2663 [eventConfig.reactEvent]: e => {
2664 log.push('---- inner');
2665 },
2666 [eventConfig.reactEvent + 'Capture']: e => {
2667 log.push('---- inner capture');
2668 },
2669 }}
2670 parentRef={node => {
2671 if (node) {
2672 // No cleanup, assume we render once.
2673 node.addEventListener(
2674 eventConfig.nativeEvent,
2675 e => {
2676 log.push('--- inner parent capture (native)');
2677 e.stopPropagation(); // <---------
2678 },
2679 {capture: true},
2680 );
2681 }
2682 }}
2683 parentProps={{
2684 [eventConfig.reactEvent]: e => {
2685 log.push('--- inner parent');
2686 },
2687 [eventConfig.reactEvent + 'Capture']: e => {
2688 log.push('--- inner parent capture');
2689 },
2690 }}
2691 outerProps={{
2692 [eventConfig.reactEvent]: e => {
2693 log.push('-- outer');
2694 },
2695 [eventConfig.reactEvent + 'Capture']: e => {
2696 log.push('-- outer capture');
2697 },
2698 }}
2699 outerParentProps={{
2700 [eventConfig.reactEvent]: e => {
2701 log.push('- outer parent');
2702 },
2703 [eventConfig.reactEvent + 'Capture']: e => {
2704 expect(e.type).toBe(eventConfig.reactEventType);
2705 log.push('- outer parent capture');
2706 },
2707 }}
2708 />,
2709 );
2710 expect(log.length).toBe(0);
2711 eventConfig.dispatch(targetRef.current);
2712 // The inner root has already received the event, so
2713 // all React capture phase listeners should run.
2714 // But then the native handler stops propagation
2715 // so none of the bubbling React handlers would run.
2716 expect(log).toEqual(unindent`
2717 - outer parent capture
2718 -- outer capture
2719 --- inner parent capture
2720 ---- inner capture
2721 --- inner parent capture (native)
2722 `);
2723 }
2724
2725 async function testNativeStopPropagationInInnerBubblePhase(eventConfig) {
2726 const log = [];
2727 const targetRef = React.createRef();
2728 await render(
2729 <Fixture
2730 type={eventConfig.type}
2731 targetRef={node => {
2732 targetRef.current = node;
2733 if (node) {
2734 // No cleanup, assume we render once.
2735 node.addEventListener(eventConfig.nativeEvent, e => {
2736 log.push('---- inner (native)');
2737 e.stopPropagation(); // <---------
2738 });
2739 }
2740 }}
2741 targetProps={{
2742 [eventConfig.reactEvent]: e => {
2743 log.push('---- inner');
2744 },
2745 [eventConfig.reactEvent + 'Capture']: e => {
2746 log.push('---- inner capture');
2747 },
2748 }}
2749 parentProps={{
2750 [eventConfig.reactEvent]: e => {
2751 log.push('--- inner parent');
2752 },
2753 [eventConfig.reactEvent + 'Capture']: e => {
2754 log.push('--- inner parent capture');
2755 },
2756 }}
2757 outerProps={{
2758 [eventConfig.reactEvent]: e => {
2759 log.push('-- outer');
2760 },
2761 [eventConfig.reactEvent + 'Capture']: e => {
2762 log.push('-- outer capture');
2763 },
2764 }}
2765 outerParentProps={{
2766 [eventConfig.reactEvent]: e => {
2767 log.push('- outer parent');
2768 },
2769 [eventConfig.reactEvent + 'Capture']: e => {
2770 expect(e.type).toBe(eventConfig.reactEventType);
2771 log.push('- outer parent capture');
2772 },
2773 }}
2774 />,
2775 );
2776 expect(log.length).toBe(0);
2777 eventConfig.dispatch(targetRef.current);
2778 // The capture phase is entirely unaffected.
2779 // Then, we get into the bubble phase.
2780 // We start with the native innermost handler.
2781 // It stops propagation, so nothing else happens.
2782 expect(log).toEqual(unindent`
2783 - outer parent capture
2784 -- outer capture
2785 --- inner parent capture
2786 ---- inner capture
2787 ---- inner (native)
2788 `);
2789 }
2790
2791 async function testNativeStopPropagationInInnerEmulatedBubblePhase(
2792 eventConfig,
2793 ) {
2794 const log = [];
2795 const targetRef = React.createRef();
2796 await render(
2797 <Fixture
2798 type={eventConfig.type}
2799 targetRef={node => {
2800 targetRef.current = node;
2801 if (node) {
2802 // No cleanup, assume we render once.
2803 node.addEventListener(eventConfig.nativeEvent, e => {
2804 log.push('---- inner (native)');
2805 e.stopPropagation(); // <---------
2806 });
2807 }
2808 }}
2809 targetProps={{
2810 ...eventConfig.targetProps,
2811 [eventConfig.reactEvent]: e => {
2812 log.push('---- inner');
2813 },
2814 [eventConfig.reactEvent + 'Capture']: e => {
2815 log.push('---- inner capture');
2816 },
2817 }}
2818 parentProps={{
2819 [eventConfig.reactEvent]: e => {
2820 log.push('--- inner parent');
2821 },
2822 [eventConfig.reactEvent + 'Capture']: e => {
2823 log.push('--- inner parent capture');
2824 },
2825 }}
2826 outerProps={{
2827 [eventConfig.reactEvent]: e => {
2828 log.push('-- outer');
2829 },
2830 [eventConfig.reactEvent + 'Capture']: e => {
2831 log.push('-- outer capture');
2832 },
2833 }}
2834 outerParentProps={{
2835 [eventConfig.reactEvent]: e => {
2836 log.push('- outer parent');
2837 },
2838 [eventConfig.reactEvent + 'Capture']: e => {
2839 expect(e.type).toBe(eventConfig.reactEventType);
2840 log.push('- outer parent capture');
2841 },
2842 }}
2843 />,
2844 );
2845 expect(log.length).toBe(0);
2846 eventConfig.dispatch(targetRef.current);
2847 // This event does not natively bubble, so React
2848 // attaches the listener directly to the element.
2849 // As a result, by the time our custom native listener
2850 // fires, it is too late to do anything -- the React
2851 // emulated bubbilng has already happened.
2852 expect(log).toEqual(unindent`
2853 - outer parent capture
2854 -- outer capture
2855 --- inner parent capture
2856 ---- inner capture
2857 ---- inner
2858 --- inner parent
2859 ---- inner (native)
2860 `);
2861 }
2862
2863 async function testNativeStopPropagationInOuterBubblePhase(eventConfig) {
2864 const log = [];
2865 const targetRef = React.createRef();
2866 await render(
2867 <Fixture
2868 type={eventConfig.type}
2869 targetRef={targetRef}
2870 targetProps={{
2871 [eventConfig.reactEvent]: e => {
2872 log.push('---- inner');
2873 },
2874 [eventConfig.reactEvent + 'Capture']: e => {
2875 log.push('---- inner capture');
2876 },
2877 }}
2878 parentProps={{
2879 [eventConfig.reactEvent]: e => {
2880 log.push('--- inner parent');
2881 },
2882 [eventConfig.reactEvent + 'Capture']: e => {
2883 log.push('--- inner parent capture');
2884 },
2885 }}
2886 outerRef={node => {
2887 if (node) {
2888 // No cleanup, assume we render once.
2889 node.addEventListener(eventConfig.nativeEvent, e => {
2890 log.push('-- outer (native)');
2891 e.stopPropagation(); // <---------
2892 });
2893 }
2894 }}
2895 outerProps={{
2896 [eventConfig.reactEvent]: e => {
2897 log.push('-- outer');
2898 },
2899 [eventConfig.reactEvent + 'Capture']: e => {
2900 log.push('-- outer capture');
2901 },
2902 }}
2903 outerParentProps={{
2904 [eventConfig.reactEvent]: e => {
2905 log.push('- outer parent');
2906 },
2907 [eventConfig.reactEvent + 'Capture']: e => {
2908 expect(e.type).toBe(eventConfig.reactEventType);
2909 log.push('- outer parent capture');
2910 },
2911 }}
2912 />,
2913 );
2914 expect(log.length).toBe(0);
2915 eventConfig.dispatch(targetRef.current);
2916 // The event bubbles upwards through the inner tree.
2917 // Then it reaches the native handler which stops propagation.
2918 // As a result, it never reaches the outer React root,
2919 // and thus the outer React event handlers don't fire.
2920 expect(log).toEqual(unindent`
2921 - outer parent capture
2922 -- outer capture
2923 --- inner parent capture
2924 ---- inner capture
2925 ---- inner
2926 --- inner parent
2927 -- outer (native)
2928 `);
2929 }
2930
2931 function Fixture({
2932 type,
2933 targetRef,
2934 targetProps,
2935 parentRef,
2936 parentProps,
2937 outerRef,
2938 outerProps,
2939 outerParentRef,
2940 outerParentProps,
2941 }) {
2942 const inner = React.useMemo(
2943 () => (
2944 <Inner
2945 type={type}
2946 targetRef={targetRef}
2947 targetProps={targetProps}
2948 parentRef={parentRef}
2949 parentProps={parentProps}
2950 />
2951 ),
2952 [type, targetRef, targetProps, parentProps],
2953 );
2954 return (
2955 <Outer
2956 outerRef={outerRef}
2957 outerProps={outerProps}
2958 outerParentRef={outerParentRef}
2959 outerParentProps={outerParentProps}>
2960 <NestedReact>{inner}</NestedReact>
2961 </Outer>
2962 );
2963 }
2964
2965 function NestedReact({children}) {
2966 const ref = React.useRef();
2967 React.useLayoutEffect(() => {
2968 const parent = ref.current;
2969 const innerContainer = document.createElement('div');
2970 parent.appendChild(innerContainer);
2971 const innerReactRoot = InnerReactDOMClient.createRoot(innerContainer);
2972 InnerReactDOM.flushSync(() => {
2973 innerReactRoot.render(children);
2974 });
2975 return () => {
2976 innerReactRoot.unmount();
2977 parent.removeChild(innerContainer);
2978 };
2979 }, [children, ref]);
2980 return <div ref={ref} />;
2981 }
2982
2983 function Inner({type, targetRef, targetProps, parentRef, parentProps}) {
2984 const T = type;
2985 return (
2986 <div {...parentProps} ref={parentRef}>
2987 <T {...targetProps} ref={targetRef} />
2988 </div>
2989 );
2990 }
2991
2992 function Outer({
2993 outerRef,
2994 outerProps,
2995 outerParentProps,
2996 outerParentRef,
2997 children,
2998 }) {
2999 return (
3000 <div {...outerParentProps} ref={outerParentRef}>
3001 <div {...outerProps} ref={outerRef}>
3002 {children}
3003 </div>
3004 </div>
3005 );
3006 }
3007
3008 function unindent(str) {
3009 return str[0]
3010 .split('\n')
3011 .map(s => s.trim())
3012 .filter(s => s !== '');
3013 }
3014 });