@samitouri / QOS-React / commits / 0eea16601c

Event propagation test suite (#19483)

Dan Abramov committed Jul 29, 2020 at 15:34 UTC 0eea16601cab75dbb698d67220bce5674a13e38a
1 file changed +1327
packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js new
+1327
@@ -0,0 +1,1327 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 OuterReactDOM;
13 + let InnerReactDOM;
14 + let container;
15 +
16 + beforeEach(() => {
17 + jest.resetModules();
18 + React = require('react');
19 + jest.isolateModules(() => {
20 + OuterReactDOM = require('react-dom');
21 + });
22 + jest.isolateModules(() => {
23 + InnerReactDOM = require('react-dom');
24 + });
25 + expect(OuterReactDOM).not.toBe(InnerReactDOM);
26 + });
27 +
28 + afterEach(() => {
29 + cleanup();
30 + });
31 +
32 + function cleanup() {
33 + if (container) {
34 + OuterReactDOM.unmountComponentAtNode(container);
35 + document.body.removeChild(container);
36 + container = null;
37 + }
38 + }
39 +
40 + function render(tree) {
41 + cleanup();
42 + container = document.createElement('div');
43 + document.body.appendChild(container);
44 + OuterReactDOM.render(tree, container);
45 + }
46 +
47 + describe('bubbling events', () => {
48 + // This test will fail in legacy mode (only used in WWW)
49 + // because we emulate the React 16 behavior where
50 + // the click handler is attached to the document.
51 + // @gate !enableLegacyFBSupport
52 + it('onClick', () => {
53 + testNativeBubblingEvent({
54 + type: 'div',
55 + reactEvent: 'onClick',
56 + nativeEvent: 'click',
57 + dispatch(node) {
58 + node.click();
59 + },
60 + });
61 + });
62 +
63 + it('onKeyPress', () => {
64 + testNativeBubblingEvent({
65 + type: 'input',
66 + reactEvent: 'onKeyPress',
67 + nativeEvent: 'keypress',
68 + dispatch(node) {
69 + node.dispatchEvent(
70 + new KeyboardEvent('keypress', {
71 + keyCode: 13,
72 + bubbles: true,
73 + cancelable: true,
74 + }),
75 + );
76 + },
77 + });
78 + });
79 +
80 + it('onMouseDown', () => {
81 + testNativeBubblingEvent({
82 + type: 'button',
83 + reactEvent: 'onMouseDown',
84 + nativeEvent: 'mousedown',
85 + dispatch(node) {
86 + node.dispatchEvent(
87 + new MouseEvent('mousedown', {
88 + bubbles: true,
89 + cancelable: true,
90 + }),
91 + );
92 + },
93 + });
94 + });
95 +
96 + it('onTouchStart', () => {
97 + testNativeBubblingEvent({
98 + type: 'div',
99 + reactEvent: 'onTouchStart',
100 + nativeEvent: 'touchstart',
101 + dispatch(node) {
102 + node.dispatchEvent(
103 + new Event('touchstart', {
104 + bubbles: true,
105 + cancelable: true,
106 + }),
107 + );
108 + },
109 + });
110 + });
111 +
112 + it('onWheel', () => {
113 + testNativeBubblingEvent({
114 + type: 'div',
115 + reactEvent: 'onWheel',
116 + nativeEvent: 'wheel',
117 + dispatch(node) {
118 + node.dispatchEvent(
119 + new Event('wheel', {
120 + bubbles: true,
121 + cancelable: true,
122 + }),
123 + );
124 + },
125 + });
126 + });
127 +
128 + it('onSubmit', () => {
129 + testNativeBubblingEvent({
130 + type: 'form',
131 + reactEvent: 'onSubmit',
132 + nativeEvent: 'submit',
133 + dispatch(node) {
134 + const e = new Event('submit', {
135 + bubbles: true,
136 + cancelable: true,
137 + });
138 + node.dispatchEvent(e);
139 + },
140 + });
141 + });
142 +
143 + it('onReset', () => {
144 + testNativeBubblingEvent({
145 + type: 'form',
146 + reactEvent: 'onReset',
147 + nativeEvent: 'reset',
148 + dispatch(node) {
149 + const e = new Event('reset', {
150 + bubbles: true,
151 + cancelable: true,
152 + });
153 + node.dispatchEvent(e);
154 + },
155 + });
156 + });
157 +
158 + it('onFocus', () => {
159 + testNativeBubblingEvent({
160 + type: 'input',
161 + reactEvent: 'onFocus',
162 + nativeEvent: 'focusin',
163 + dispatch(node) {
164 + const e = new Event('focusin', {
165 + bubbles: true,
166 + cancelable: true,
167 + });
168 + node.dispatchEvent(e);
169 + },
170 + });
171 + });
172 +
173 + it('onBlur', () => {
174 + testNativeBubblingEvent({
175 + type: 'input',
176 + reactEvent: 'onBlur',
177 + nativeEvent: 'focusout',
178 + dispatch(node) {
179 + const e = new Event('focusout', {
180 + bubbles: true,
181 + cancelable: true,
182 + });
183 + node.dispatchEvent(e);
184 + },
185 + });
186 + });
187 + });
188 +
189 + describe('non-bubbling events that bubble in React', () => {
190 + it('onInvalid', () => {
191 + testEmulatedBubblingEvent({
192 + type: 'input',
193 + reactEvent: 'onInvalid',
194 + nativeEvent: 'invalid',
195 + dispatch(node) {
196 + const e = new Event('invalid', {
197 + bubbles: false,
198 + cancelable: true,
199 + });
200 + node.dispatchEvent(e);
201 + },
202 + });
203 + });
204 +
205 + it('onLoad', () => {
206 + testEmulatedBubblingEvent({
207 + type: 'img',
208 + reactEvent: 'onLoad',
209 + nativeEvent: 'load',
210 + dispatch(node) {
211 + const e = new Event('load', {
212 + bubbles: false,
213 + cancelable: true,
214 + });
215 + node.dispatchEvent(e);
216 + },
217 + });
218 + });
219 +
220 + it('onError', () => {
221 + testEmulatedBubblingEvent({
222 + type: 'img',
223 + reactEvent: 'onError',
224 + nativeEvent: 'error',
225 + dispatch(node) {
226 + const e = new Event('error', {
227 + bubbles: false,
228 + cancelable: true,
229 + });
230 + node.dispatchEvent(e);
231 + },
232 + });
233 + });
234 +
235 + it('onClose', () => {
236 + testEmulatedBubblingEvent({
237 + type: 'dialog',
238 + reactEvent: 'onClose',
239 + nativeEvent: 'close',
240 + dispatch(node) {
241 + const e = new Event('close', {
242 + bubbles: false,
243 + cancelable: true,
244 + });
245 + node.dispatchEvent(e);
246 + },
247 + });
248 + });
249 +
250 + it('onCancel', () => {
251 + testEmulatedBubblingEvent({
252 + type: 'dialog',
253 + reactEvent: 'onCancel',
254 + nativeEvent: 'cancel',
255 + dispatch(node) {
256 + const e = new Event('cancel', {
257 + bubbles: false,
258 + cancelable: true,
259 + });
260 + node.dispatchEvent(e);
261 + },
262 + });
263 + });
264 +
265 + it('onPlay', () => {
266 + testEmulatedBubblingEvent({
267 + type: 'video',
268 + reactEvent: 'onPlay',
269 + nativeEvent: 'play',
270 + dispatch(node) {
271 + const e = new Event('play', {
272 + bubbles: false,
273 + cancelable: true,
274 + });
275 + node.dispatchEvent(e);
276 + },
277 + });
278 + });
279 + });
280 +
281 + describe('non-bubbling events that do not bubble in React', () => {
282 + // This test will fail outside of the no-bubbling flag
283 + // because its bubbling emulation is currently broken.
284 + // In particular, if the target itself doesn't have
285 + // a handler, it will not emulate bubbling correctly.
286 + // Instead of fixing this, we'll just turn this flag on.
287 + // @gate disableOnScrollBubbling
288 + it('onScroll', () => {
289 + testNonBubblingEvent({
290 + type: 'div',
291 + reactEvent: 'onScroll',
292 + nativeEvent: 'scroll',
293 + dispatch(node) {
294 + const e = new Event('scroll', {
295 + bubbles: false,
296 + cancelable: true,
297 + });
298 + node.dispatchEvent(e);
299 + },
300 + });
301 + });
302 + });
303 +
304 + // Events that bubble in React and in the browser.
305 + // React delegates them to the root.
306 + function testNativeBubblingEvent(config) {
307 + testNativeBubblingEventWithTargetListener(config);
308 + testNativeBubblingEventWithoutTargetListener(config);
309 + testReactStopPropagationInOuterCapturePhase(config);
310 + testReactStopPropagationInInnerCapturePhase(config);
311 + testReactStopPropagationInInnerBubblePhase(config);
312 + testReactStopPropagationInOuterBubblePhase(config);
313 + testNativeStopPropagationInOuterCapturePhase(config);
314 + testNativeStopPropagationInInnerCapturePhase(config);
315 + testNativeStopPropagationInInnerBubblePhase(config);
316 + testNativeStopPropagationInOuterBubblePhase(config);
317 + }
318 +
319 + // Events that bubble in React but not in the browser.
320 + // React attaches them to the elements.
321 + function testEmulatedBubblingEvent(config) {
322 + testEmulatedBubblingEventWithTargetListener(config);
323 + testEmulatedBubblingEventWithoutTargetListener(config);
324 + testReactStopPropagationInOuterCapturePhase(config);
325 + testReactStopPropagationInInnerCapturePhase(config);
326 + testReactStopPropagationInInnerBubblePhase(config);
327 + testNativeStopPropagationInOuterCapturePhase(config);
328 + testNativeStopPropagationInInnerCapturePhase(config);
329 + testNativeStopPropagationInInnerEmulatedBubblePhase(config);
330 + }
331 +
332 + // Events that don't bubble either in React or in the browser.
333 + function testNonBubblingEvent(config) {
334 + testNonBubblingEventWithTargetListener(config);
335 + testNonBubblingEventWithoutTargetListener(config);
336 + testReactStopPropagationInOuterCapturePhase(config);
337 + testReactStopPropagationInInnerCapturePhase(config);
338 + testReactStopPropagationInInnerBubblePhase(config);
339 + testNativeStopPropagationInOuterCapturePhase(config);
340 + testNativeStopPropagationInInnerCapturePhase(config);
341 + }
342 +
343 + function testNativeBubblingEventWithTargetListener(eventConfig) {
344 + const log = [];
345 + const targetRef = React.createRef();
346 + render(
347 + <Fixture
348 + type={eventConfig.type}
349 + targetRef={targetRef}
350 + targetProps={{
351 + [eventConfig.reactEvent]: e => {
352 + log.push('---- inner');
353 + },
354 + [eventConfig.reactEvent + 'Capture']: e => {
355 + log.push('---- inner capture');
356 + },
357 + }}
358 + parentProps={{
359 + [eventConfig.reactEvent]: e => {
360 + log.push('--- inner parent');
361 + },
362 + [eventConfig.reactEvent + 'Capture']: e => {
363 + log.push('--- inner parent capture');
364 + },
365 + }}
366 + outerProps={{
367 + [eventConfig.reactEvent]: e => {
368 + log.push('-- outer');
369 + },
370 + [eventConfig.reactEvent + 'Capture']: e => {
371 + log.push('-- outer capture');
372 + },
373 + }}
374 + outerParentProps={{
375 + [eventConfig.reactEvent]: e => {
376 + log.push('- outer parent');
377 + },
378 + [eventConfig.reactEvent + 'Capture']: e => {
379 + log.push('- outer parent capture');
380 + },
381 + }}
382 + />,
383 + );
384 + expect(log.length).toBe(0);
385 + eventConfig.dispatch(targetRef.current);
386 + // Should print all listeners.
387 + expect(log).toEqual(unindent`
388 + - outer parent capture
389 + -- outer capture
390 + --- inner parent capture
391 + ---- inner capture
392 + ---- inner
393 + --- inner parent
394 + -- outer
395 + - outer parent
396 + `);
397 + }
398 +
399 + function testEmulatedBubblingEventWithTargetListener(eventConfig) {
400 + const log = [];
401 + const targetRef = React.createRef();
402 + render(
403 + <Fixture
404 + type={eventConfig.type}
405 + targetRef={targetRef}
406 + targetProps={{
407 + [eventConfig.reactEvent]: e => {
408 + log.push('---- inner');
409 + },
410 + [eventConfig.reactEvent + 'Capture']: e => {
411 + log.push('---- inner capture');
412 + },
413 + }}
414 + parentProps={{
415 + [eventConfig.reactEvent]: e => {
416 + log.push('--- inner parent');
417 + },
418 + [eventConfig.reactEvent + 'Capture']: e => {
419 + log.push('--- inner parent capture');
420 + },
421 + }}
422 + outerProps={{
423 + [eventConfig.reactEvent]: e => {
424 + log.push('-- outer');
425 + },
426 + [eventConfig.reactEvent + 'Capture']: e => {
427 + log.push('-- outer capture');
428 + },
429 + }}
430 + outerParentProps={{
431 + [eventConfig.reactEvent]: e => {
432 + log.push('- outer parent');
433 + },
434 + [eventConfig.reactEvent + 'Capture']: e => {
435 + log.push('- outer parent capture');
436 + },
437 + }}
438 + />,
439 + );
440 + expect(log.length).toBe(0);
441 + eventConfig.dispatch(targetRef.current);
442 + // This event doesn't bubble natively, but React emulates it.
443 + // Since the element is created by the inner React, the bubbling
444 + // stops at the inner parent and never reaches the outer React.
445 + // In the future, we might consider not bubbling these events
446 + // at all, in in which case inner parent also wouldn't be logged.
447 + expect(log).toEqual(unindent`
448 + - outer parent capture
449 + -- outer capture
450 + --- inner parent capture
451 + ---- inner capture
452 + ---- inner
453 + --- inner parent
454 + `);
455 + }
456 +
457 + function testNonBubblingEventWithTargetListener(eventConfig) {
458 + const log = [];
459 + const targetRef = React.createRef();
460 + render(
461 + <Fixture
462 + type={eventConfig.type}
463 + targetRef={targetRef}
464 + targetProps={{
465 + [eventConfig.reactEvent]: e => {
466 + log.push('---- inner');
467 + },
468 + [eventConfig.reactEvent + 'Capture']: e => {
469 + log.push('---- inner capture');
470 + },
471 + }}
472 + parentProps={{
473 + [eventConfig.reactEvent]: e => {
474 + log.push('--- inner parent');
475 + },
476 + [eventConfig.reactEvent + 'Capture']: e => {
477 + log.push('--- inner parent capture');
478 + },
479 + }}
480 + outerProps={{
481 + [eventConfig.reactEvent]: e => {
482 + log.push('-- outer');
483 + },
484 + [eventConfig.reactEvent + 'Capture']: e => {
485 + log.push('-- outer capture');
486 + },
487 + }}
488 + outerParentProps={{
489 + [eventConfig.reactEvent]: e => {
490 + log.push('- outer parent');
491 + },
492 + [eventConfig.reactEvent + 'Capture']: e => {
493 + log.push('- outer parent capture');
494 + },
495 + }}
496 + />,
497 + );
498 + expect(log.length).toBe(0);
499 + eventConfig.dispatch(targetRef.current);
500 + // This event doesn't bubble natively, and React is
501 + // not emulating it either. So it only reaches the
502 + // target and stops there.
503 + expect(log).toEqual(unindent`
504 + - outer parent capture
505 + -- outer capture
506 + --- inner parent capture
507 + ---- inner capture
508 + ---- inner
509 + `);
510 + }
511 +
512 + function testNativeBubblingEventWithoutTargetListener(eventConfig) {
513 + const log = [];
514 + const targetRef = React.createRef();
515 + render(
516 + <Fixture
517 + type={eventConfig.type}
518 + targetRef={targetRef}
519 + targetProps={
520 + {
521 + // No listener on the target itself.
522 + }
523 + }
524 + parentProps={{
525 + [eventConfig.reactEvent]: e => {
526 + log.push('--- inner parent');
527 + },
528 + [eventConfig.reactEvent + 'Capture']: e => {
529 + log.push('--- inner parent capture');
530 + },
531 + }}
532 + outerProps={{
533 + [eventConfig.reactEvent]: e => {
534 + log.push('-- outer');
535 + },
536 + [eventConfig.reactEvent + 'Capture']: e => {
537 + log.push('-- outer capture');
538 + },
539 + }}
540 + outerParentProps={{
541 + [eventConfig.reactEvent]: e => {
542 + log.push('- outer parent');
543 + },
544 + [eventConfig.reactEvent + 'Capture']: e => {
545 + log.push('- outer parent capture');
546 + },
547 + }}
548 + />,
549 + );
550 + expect(log.length).toBe(0);
551 + eventConfig.dispatch(targetRef.current);
552 + // Should print all listeners except the innermost one.
553 + expect(log).toEqual(unindent`
554 + - outer parent capture
555 + -- outer capture
556 + --- inner parent capture
557 + --- inner parent
558 + -- outer
559 + - outer parent
560 + `);
561 + }
562 +
563 + function testEmulatedBubblingEventWithoutTargetListener(eventConfig) {
564 + const log = [];
565 + const targetRef = React.createRef();
566 + render(
567 + <Fixture
568 + type={eventConfig.type}
569 + targetRef={targetRef}
570 + targetProps={
571 + {
572 + // No listener on the target itself.
573 + }
574 + }
575 + parentProps={{
576 + [eventConfig.reactEvent]: e => {
577 + log.push('--- inner parent');
578 + },
579 + [eventConfig.reactEvent + 'Capture']: e => {
580 + log.push('--- inner parent capture');
581 + },
582 + }}
583 + outerProps={{
584 + [eventConfig.reactEvent]: e => {
585 + log.push('-- outer');
586 + },
587 + [eventConfig.reactEvent + 'Capture']: e => {
588 + log.push('-- outer capture');
589 + },
590 + }}
591 + outerParentProps={{
592 + [eventConfig.reactEvent]: e => {
593 + log.push('- outer parent');
594 + },
595 + [eventConfig.reactEvent + 'Capture']: e => {
596 + log.push('- outer parent capture');
597 + },
598 + }}
599 + />,
600 + );
601 + expect(log.length).toBe(0);
602 + eventConfig.dispatch(targetRef.current);
603 + // This event doesn't bubble natively, but React emulates it.
604 + // Since the element is created by the inner React, the bubbling
605 + // stops at the inner parent and never reaches the outer React.
606 + // In the future, we might consider not bubbling these events
607 + // at all, in in which case inner parent also wouldn't be logged.
608 + expect(log).toEqual(unindent`
609 + - outer parent capture
610 + -- outer capture
611 + --- inner parent capture
612 + --- inner parent
613 + `);
614 + }
615 +
616 + function testNonBubblingEventWithoutTargetListener(eventConfig) {
617 + const log = [];
618 + const targetRef = React.createRef();
619 + render(
620 + <Fixture
621 + type={eventConfig.type}
622 + targetRef={targetRef}
623 + targetProps={
624 + {
625 + // No listener on the target itself.
626 + }
627 + }
628 + parentProps={{
629 + [eventConfig.reactEvent]: e => {
630 + log.push('--- inner parent');
631 + },
632 + [eventConfig.reactEvent + 'Capture']: e => {
633 + log.push('--- inner parent capture');
634 + },
635 + }}
636 + outerProps={{
637 + [eventConfig.reactEvent]: e => {
638 + log.push('-- outer');
639 + },
640 + [eventConfig.reactEvent + 'Capture']: e => {
641 + log.push('-- outer capture');
642 + },
643 + }}
644 + outerParentProps={{
645 + [eventConfig.reactEvent]: e => {
646 + log.push('- outer parent');
647 + },
648 + [eventConfig.reactEvent + 'Capture']: e => {
649 + log.push('- outer parent capture');
650 + },
651 + }}
652 + />,
653 + );
654 + expect(log.length).toBe(0);
655 + eventConfig.dispatch(targetRef.current);
656 + // This event doesn't bubble native, and React doesn't
657 + // emulate bubbling either. Since we don't have a target
658 + // listener, only capture phase listeners fire.
659 + expect(log).toEqual(unindent`
660 + - outer parent capture
661 + -- outer capture
662 + --- inner parent capture
663 + `);
664 + }
665 +
666 + function testReactStopPropagationInOuterCapturePhase(eventConfig) {
667 + const log = [];
668 + const targetRef = React.createRef();
669 + render(
670 + <Fixture
671 + type={eventConfig.type}
672 + targetRef={node => {
673 + targetRef.current = node;
674 + if (node) {
675 + // No cleanup, assume we render once.
676 + node.addEventListener(eventConfig.nativeEvent, e => {
677 + // We *don't* expect this to appear in the log
678 + // at all because the event is stopped earlier.
679 + log.push('---- inner (native)');
680 + });
681 + }
682 + }}
683 + targetProps={{
684 + [eventConfig.reactEvent]: e => {
685 + log.push('---- inner');
686 + },
687 + [eventConfig.reactEvent + 'Capture']: e => {
688 + log.push('---- inner capture');
689 + },
690 + }}
691 + parentProps={{
692 + [eventConfig.reactEvent]: e => {
693 + log.push('--- inner parent');
694 + },
695 + [eventConfig.reactEvent + 'Capture']: e => {
696 + log.push('--- inner parent capture');
697 + },
698 + }}
699 + outerProps={{
700 + [eventConfig.reactEvent]: e => {
701 + log.push('-- outer');
702 + },
703 + [eventConfig.reactEvent + 'Capture']: e => {
704 + e.stopPropagation(); // <---------
705 + log.push('-- outer capture');
706 + },
707 + }}
708 + outerParentProps={{
709 + [eventConfig.reactEvent]: e => {
710 + log.push('- outer parent');
711 + },
712 + [eventConfig.reactEvent + 'Capture']: e => {
713 + log.push('- outer parent capture');
714 + },
715 + }}
716 + />,
717 + );
718 + expect(log.length).toBe(0);
719 + eventConfig.dispatch(targetRef.current);
720 + // Should stop at the outer capture.
721 + // We don't get to the inner root at all.
722 + expect(log).toEqual(unindent`
723 + - outer parent capture
724 + -- outer capture
725 + `);
726 + }
727 +
728 + function testReactStopPropagationInInnerCapturePhase(eventConfig) {
729 + const log = [];
730 + const targetRef = React.createRef();
731 + render(
732 + <Fixture
733 + type={eventConfig.type}
734 + targetRef={node => {
735 + targetRef.current = node;
736 + if (node) {
737 + // No cleanup, assume we render once.
738 + node.addEventListener(eventConfig.nativeEvent, e => {
739 + // We *don't* expect this to appear in the log
740 + // at all because the event is stopped earlier.
741 + log.push('---- inner (native)');
742 + });
743 + }
744 + }}
745 + targetProps={{
746 + [eventConfig.reactEvent]: e => {
747 + log.push('---- inner');
748 + },
749 + [eventConfig.reactEvent + 'Capture']: e => {
750 + log.push('---- inner capture');
751 + },
752 + }}
753 + parentProps={{
754 + [eventConfig.reactEvent]: e => {
755 + log.push('--- inner parent');
756 + },
757 + [eventConfig.reactEvent + 'Capture']: e => {
758 + e.stopPropagation(); // <---------
759 + log.push('--- inner parent capture');
760 + },
761 + }}
762 + outerProps={{
763 + [eventConfig.reactEvent]: e => {
764 + log.push('-- outer');
765 + },
766 + [eventConfig.reactEvent + 'Capture']: e => {
767 + log.push('-- outer capture');
768 + },
769 + }}
770 + outerParentProps={{
771 + [eventConfig.reactEvent]: e => {
772 + log.push('- outer parent');
773 + },
774 + [eventConfig.reactEvent + 'Capture']: e => {
775 + log.push('- outer parent capture');
776 + },
777 + }}
778 + />,
779 + );
780 + expect(log.length).toBe(0);
781 + eventConfig.dispatch(targetRef.current);
782 + // We get to the inner root, but we don't
783 + // get to the target and we don't bubble.
784 + expect(log).toEqual(unindent`
785 + - outer parent capture
786 + -- outer capture
787 + --- inner parent capture
788 + `);
789 + }
790 +
791 + function testReactStopPropagationInInnerBubblePhase(eventConfig) {
792 + const log = [];
793 + const targetRef = React.createRef();
794 + render(
795 + <Fixture
796 + type={eventConfig.type}
797 + targetRef={targetRef}
798 + targetProps={{
799 + [eventConfig.reactEvent]: e => {
800 + e.stopPropagation(); // <---------
801 + log.push('---- inner');
802 + },
803 + [eventConfig.reactEvent + 'Capture']: e => {
804 + log.push('---- inner capture');
805 + },
806 + }}
807 + parentProps={{
808 + [eventConfig.reactEvent]: e => {
809 + log.push('--- inner parent');
810 + },
811 + [eventConfig.reactEvent + 'Capture']: e => {
812 + log.push('--- inner parent capture');
813 + },
814 + }}
815 + outerRef={node => {
816 + if (node) {
817 + // No cleanup, assume we render once.
818 + node.addEventListener(eventConfig.nativeEvent, e => {
819 + // We *don't* expect this to appear in the log
820 + // at all because the event is stopped earlier.
821 + log.push('-- outer (native)');
822 + });
823 + }
824 + }}
825 + outerProps={{
826 + [eventConfig.reactEvent]: e => {
827 + log.push('-- outer');
828 + },
829 + [eventConfig.reactEvent + 'Capture']: e => {
830 + log.push('-- outer capture');
831 + },
832 + }}
833 + outerParentProps={{
834 + [eventConfig.reactEvent]: e => {
835 + log.push('- outer parent');
836 + },
837 + [eventConfig.reactEvent + 'Capture']: e => {
838 + log.push('- outer parent capture');
839 + },
840 + }}
841 + />,
842 + );
843 + expect(log.length).toBe(0);
844 + eventConfig.dispatch(targetRef.current);
845 + // Should stop at the target and not go further.
846 + expect(log).toEqual(unindent`
847 + - outer parent capture
848 + -- outer capture
849 + --- inner parent capture
850 + ---- inner capture
851 + ---- inner
852 + `);
853 + }
854 +
855 + function testReactStopPropagationInOuterBubblePhase(eventConfig) {
856 + const log = [];
857 + const targetRef = React.createRef();
858 + render(
859 + <Fixture
860 + type={eventConfig.type}
861 + targetRef={targetRef}
862 + targetProps={{
863 + [eventConfig.reactEvent]: e => {
864 + log.push('---- inner');
865 + },
866 + [eventConfig.reactEvent + 'Capture']: e => {
867 + log.push('---- inner capture');
868 + },
869 + }}
870 + parentProps={{
871 + [eventConfig.reactEvent]: e => {
872 + log.push('--- inner parent');
873 + },
874 + [eventConfig.reactEvent + 'Capture']: e => {
875 + log.push('--- inner parent capture');
876 + },
877 + }}
878 + outerProps={{
879 + [eventConfig.reactEvent]: e => {
880 + e.stopPropagation(); // <---------
881 + log.push('-- outer');
882 + },
883 + [eventConfig.reactEvent + 'Capture']: e => {
884 + log.push('-- outer capture');
885 + },
886 + }}
887 + outerParentProps={{
888 + [eventConfig.reactEvent]: e => {
889 + log.push('- outer parent');
890 + },
891 + [eventConfig.reactEvent + 'Capture']: e => {
892 + log.push('- outer parent capture');
893 + },
894 + }}
895 + />,
896 + );
897 + expect(log.length).toBe(0);
898 + eventConfig.dispatch(targetRef.current);
899 + // Should not reach the parent outer bubble handler.
900 + expect(log).toEqual(unindent`
901 + - outer parent capture
902 + -- outer capture
903 + --- inner parent capture
904 + ---- inner capture
905 + ---- inner
906 + --- inner parent
907 + -- outer
908 + `);
909 + }
910 +
911 + function testNativeStopPropagationInOuterCapturePhase(eventConfig) {
912 + const log = [];
913 + const targetRef = React.createRef();
914 + render(
915 + <Fixture
916 + type={eventConfig.type}
917 + targetRef={targetRef}
918 + targetProps={{
919 + [eventConfig.reactEvent]: e => {
920 + log.push('---- inner');
921 + },
922 + [eventConfig.reactEvent + 'Capture']: e => {
923 + log.push('---- inner capture');
924 + },
925 + }}
926 + parentProps={{
927 + [eventConfig.reactEvent]: e => {
928 + log.push('--- inner parent');
929 + },
930 + [eventConfig.reactEvent + 'Capture']: e => {
931 + log.push('--- inner parent capture');
932 + },
933 + }}
934 + outerProps={{
935 + [eventConfig.reactEvent]: e => {
936 + log.push('-- outer');
937 + },
938 + [eventConfig.reactEvent + 'Capture']: e => {
939 + log.push('-- outer capture');
940 + },
941 + }}
942 + outerParentRef={node => {
943 + if (node) {
944 + // No cleanup, assume we render once.
945 + node.addEventListener(
946 + eventConfig.nativeEvent,
947 + e => {
948 + log.push('- outer parent capture (native)');
949 + e.stopPropagation(); // <---------
950 + },
951 + {capture: true},
952 + );
953 + }
954 + }}
955 + outerParentProps={{
956 + [eventConfig.reactEvent]: e => {
957 + log.push('- outer parent');
958 + },
959 + [eventConfig.reactEvent + 'Capture']: e => {
960 + log.push('- outer parent capture');
961 + },
962 + }}
963 + />,
964 + );
965 + expect(log.length).toBe(0);
966 + eventConfig.dispatch(targetRef.current);
967 + // The outer root has already received the event,
968 + // so the capture phrase runs for it. But the inner
969 + // root is prevented from receiving it by the native
970 + // handler in the outer native capture phase.
971 + expect(log).toEqual(unindent`
972 + - outer parent capture
973 + -- outer capture
974 + - outer parent capture (native)
975 + `);
976 + }
977 +
978 + function testNativeStopPropagationInInnerCapturePhase(eventConfig) {
979 + const log = [];
980 + const targetRef = React.createRef();
981 + render(
982 + <Fixture
983 + type={eventConfig.type}
984 + targetRef={targetRef}
985 + targetProps={{
986 + [eventConfig.reactEvent]: e => {
987 + log.push('---- inner');
988 + },
989 + [eventConfig.reactEvent + 'Capture']: e => {
990 + log.push('---- inner capture');
991 + },
992 + }}
993 + parentRef={node => {
994 + if (node) {
995 + // No cleanup, assume we render once.
996 + node.addEventListener(
997 + eventConfig.nativeEvent,
998 + e => {
999 + log.push('--- inner parent capture (native)');
1000 + e.stopPropagation(); // <---------
1001 + },
1002 + {capture: true},
1003 + );
1004 + }
1005 + }}
1006 + parentProps={{
1007 + [eventConfig.reactEvent]: e => {
1008 + log.push('--- inner parent');
1009 + },
1010 + [eventConfig.reactEvent + 'Capture']: e => {
1011 + log.push('--- inner parent capture');
1012 + },
1013 + }}
1014 + outerProps={{
1015 + [eventConfig.reactEvent]: e => {
1016 + log.push('-- outer');
1017 + },
1018 + [eventConfig.reactEvent + 'Capture']: e => {
1019 + log.push('-- outer capture');
1020 + },
1021 + }}
1022 + outerParentProps={{
1023 + [eventConfig.reactEvent]: e => {
1024 + log.push('- outer parent');
1025 + },
1026 + [eventConfig.reactEvent + 'Capture']: e => {
1027 + log.push('- outer parent capture');
1028 + },
1029 + }}
1030 + />,
1031 + );
1032 + expect(log.length).toBe(0);
1033 + eventConfig.dispatch(targetRef.current);
1034 + // The inner root has already received the event, so
1035 + // all React capture phase listeners should run.
1036 + // But then the native handler stops propagation
1037 + // so none of the bubbling React handlers would run.
1038 + expect(log).toEqual(unindent`
1039 + - outer parent capture
1040 + -- outer capture
1041 + --- inner parent capture
1042 + ---- inner capture
1043 + --- inner parent capture (native)
1044 + `);
1045 + }
1046 +
1047 + function testNativeStopPropagationInInnerBubblePhase(eventConfig) {
1048 + const log = [];
1049 + const targetRef = React.createRef();
1050 + render(
1051 + <Fixture
1052 + type={eventConfig.type}
1053 + targetRef={node => {
1054 + targetRef.current = node;
1055 + if (node) {
1056 + // No cleanup, assume we render once.
1057 + node.addEventListener(eventConfig.nativeEvent, e => {
1058 + log.push('---- inner (native)');
1059 + e.stopPropagation(); // <---------
1060 + });
1061 + }
1062 + }}
1063 + targetProps={{
1064 + [eventConfig.reactEvent]: e => {
1065 + log.push('---- inner');
1066 + },
1067 + [eventConfig.reactEvent + 'Capture']: e => {
1068 + log.push('---- inner capture');
1069 + },
1070 + }}
1071 + parentProps={{
1072 + [eventConfig.reactEvent]: e => {
1073 + log.push('--- inner parent');
1074 + },
1075 + [eventConfig.reactEvent + 'Capture']: e => {
1076 + log.push('--- inner parent capture');
1077 + },
1078 + }}
1079 + outerProps={{
1080 + [eventConfig.reactEvent]: e => {
1081 + log.push('-- outer');
1082 + },
1083 + [eventConfig.reactEvent + 'Capture']: e => {
1084 + log.push('-- outer capture');
1085 + },
1086 + }}
1087 + outerParentProps={{
1088 + [eventConfig.reactEvent]: e => {
1089 + log.push('- outer parent');
1090 + },
1091 + [eventConfig.reactEvent + 'Capture']: e => {
1092 + log.push('- outer parent capture');
1093 + },
1094 + }}
1095 + />,
1096 + );
1097 + expect(log.length).toBe(0);
1098 + eventConfig.dispatch(targetRef.current);
1099 + // The capture phase is entirely unaffected.
1100 + // Then, we get into the bubble phase.
1101 + // We start with the native innermost handler.
1102 + // It stops propagation, so nothing else happens.
1103 + expect(log).toEqual(unindent`
1104 + - outer parent capture
1105 + -- outer capture
1106 + --- inner parent capture
1107 + ---- inner capture
1108 + ---- inner (native)
1109 + `);
1110 + }
1111 +
1112 + function testNativeStopPropagationInInnerEmulatedBubblePhase(eventConfig) {
1113 + const log = [];
1114 + const targetRef = React.createRef();
1115 + render(
1116 + <Fixture
1117 + type={eventConfig.type}
1118 + targetRef={node => {
1119 + targetRef.current = node;
1120 + if (node) {
1121 + // No cleanup, assume we render once.
1122 + node.addEventListener(eventConfig.nativeEvent, e => {
1123 + log.push('---- inner (native)');
1124 + e.stopPropagation(); // <---------
1125 + });
1126 + }
1127 + }}
1128 + targetProps={{
1129 + [eventConfig.reactEvent]: e => {
1130 + log.push('---- inner');
1131 + },
1132 + [eventConfig.reactEvent + 'Capture']: e => {
1133 + log.push('---- inner capture');
1134 + },
1135 + }}
1136 + parentProps={{
1137 + [eventConfig.reactEvent]: e => {
1138 + log.push('--- inner parent');
1139 + },
1140 + [eventConfig.reactEvent + 'Capture']: e => {
1141 + log.push('--- inner parent capture');
1142 + },
1143 + }}
1144 + outerProps={{
1145 + [eventConfig.reactEvent]: e => {
1146 + log.push('-- outer');
1147 + },
1148 + [eventConfig.reactEvent + 'Capture']: e => {
1149 + log.push('-- outer capture');
1150 + },
1151 + }}
1152 + outerParentProps={{
1153 + [eventConfig.reactEvent]: e => {
1154 + log.push('- outer parent');
1155 + },
1156 + [eventConfig.reactEvent + 'Capture']: e => {
1157 + log.push('- outer parent capture');
1158 + },
1159 + }}
1160 + />,
1161 + );
1162 + expect(log.length).toBe(0);
1163 + eventConfig.dispatch(targetRef.current);
1164 + // This event does not natively bubble, so React
1165 + // attaches the listener directly to the element.
1166 + // As a result, by the time our custom native listener
1167 + // fires, it is too late to do anything -- the React
1168 + // emulated bubbilng has already happened.
1169 + expect(log).toEqual(unindent`
1170 + - outer parent capture
1171 + -- outer capture
1172 + --- inner parent capture
1173 + ---- inner capture
1174 + ---- inner
1175 + --- inner parent
1176 + ---- inner (native)
1177 + `);
1178 + }
1179 +
1180 + function testNativeStopPropagationInOuterBubblePhase(eventConfig) {
1181 + const log = [];
1182 + const targetRef = React.createRef();
1183 + render(
1184 + <Fixture
1185 + type={eventConfig.type}
1186 + targetRef={targetRef}
1187 + targetProps={{
1188 + [eventConfig.reactEvent]: e => {
1189 + log.push('---- inner');
1190 + },
1191 + [eventConfig.reactEvent + 'Capture']: e => {
1192 + log.push('---- inner capture');
1193 + },
1194 + }}
1195 + parentProps={{
1196 + [eventConfig.reactEvent]: e => {
1197 + log.push('--- inner parent');
1198 + },
1199 + [eventConfig.reactEvent + 'Capture']: e => {
1200 + log.push('--- inner parent capture');
1201 + },
1202 + }}
1203 + outerRef={node => {
1204 + if (node) {
1205 + // No cleanup, assume we render once.
1206 + node.addEventListener(eventConfig.nativeEvent, e => {
1207 + log.push('-- outer (native)');
1208 + e.stopPropagation(); // <---------
1209 + });
1210 + }
1211 + }}
1212 + outerProps={{
1213 + [eventConfig.reactEvent]: e => {
1214 + log.push('-- outer');
1215 + },
1216 + [eventConfig.reactEvent + 'Capture']: e => {
1217 + log.push('-- outer capture');
1218 + },
1219 + }}
1220 + outerParentProps={{
1221 + [eventConfig.reactEvent]: e => {
1222 + log.push('- outer parent');
1223 + },
1224 + [eventConfig.reactEvent + 'Capture']: e => {
1225 + log.push('- outer parent capture');
1226 + },
1227 + }}
1228 + />,
1229 + );
1230 + expect(log.length).toBe(0);
1231 + eventConfig.dispatch(targetRef.current);
1232 + // The event bubbles upwards through the inner tree.
1233 + // Then it reaches the native handler which stops propagation.
1234 + // As a result, it never reaches the outer React root,
1235 + // and thus the outer React event handlers don't fire.
1236 + expect(log).toEqual(unindent`
1237 + - outer parent capture
1238 + -- outer capture
1239 + --- inner parent capture
1240 + ---- inner capture
1241 + ---- inner
1242 + --- inner parent
1243 + -- outer (native)
1244 + `);
1245 + }
1246 +
1247 + function Fixture({
1248 + type,
1249 + targetRef,
1250 + targetProps,
1251 + parentRef,
1252 + parentProps,
1253 + outerRef,
1254 + outerProps,
1255 + outerParentRef,
1256 + outerParentProps,
1257 + }) {
1258 + const inner = React.useMemo(
1259 + () => (
1260 + <Inner
1261 + type={type}
1262 + targetRef={targetRef}
1263 + targetProps={targetProps}
1264 + parentRef={parentRef}
1265 + parentProps={parentProps}
1266 + />
1267 + ),
1268 + [type, targetRef, targetProps, parentProps],
1269 + );
1270 + return (
1271 + <Outer
1272 + outerRef={outerRef}
1273 + outerProps={outerProps}
1274 + outerParentRef={outerParentRef}
1275 + outerParentProps={outerParentProps}>
1276 + <NestedReact>{inner}</NestedReact>
1277 + </Outer>
1278 + );
1279 + }
1280 +
1281 + function NestedReact({children}) {
1282 + const ref = React.useRef();
1283 + React.useLayoutEffect(() => {
1284 + const parent = ref.current;
1285 + const innerContainer = document.createElement('div');
1286 + parent.appendChild(innerContainer);
1287 + InnerReactDOM.render(children, innerContainer);
1288 + return () => {
1289 + InnerReactDOM.unmountComponentAtNode(innerContainer);
1290 + parent.removeChild(innerContainer);
1291 + };
1292 + }, [children, ref]);
1293 + return <div ref={ref} />;
1294 + }
1295 +
1296 + function Inner({type, targetRef, targetProps, parentRef, parentProps}) {
1297 + const T = type;
1298 + return (
1299 + <div {...parentProps} ref={parentRef}>
1300 + <T {...targetProps} ref={targetRef} />
1301 + </div>
1302 + );
1303 + }
1304 +
1305 + function Outer({
1306 + outerRef,
1307 + outerProps,
1308 + outerParentProps,
1309 + outerParentRef,
1310 + children,
1311 + }) {
1312 + return (
1313 + <div {...outerParentProps} ref={outerParentRef}>
1314 + <div {...outerProps} ref={outerRef}>
1315 + {children}
1316 + </div>
1317 + </div>
1318 + );
1319 + }
1320 +
1321 + function unindent(str) {
1322 + return str[0]
1323 + .split('\n')
1324 + .map(s => s.trim())
1325 + .filter(s => s !== '');
1326 + }
1327 +});