diff --git a/packages/react-interactions/accessibility/src/__tests__/FocusContain-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/FocusContain-test.internal.js index f173362c8d71d..d59cf1636d70c 100644 --- a/packages/react-interactions/accessibility/src/__tests__/FocusContain-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/FocusContain-test.internal.js @@ -7,13 +7,23 @@ * @flow */ -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library'; +import {createEventTarget} from 'react-interactions/events/src/dom/event-testing-library'; let React; let ReactFeatureFlags; let FocusContain; let tabbableScopeQuery; +function tabNext(target) { + target.keydown({key: 'Tab'}); + target.keyup({key: 'Tab'}); +} + +function tabPrevious(target) { + target.keydown({key: 'Tab', shiftKey: true}); + target.keyup({key: 'Tab', shiftKey: true}); +} + describe('FocusContain', () => { beforeEach(() => { jest.resetModules(); @@ -59,13 +69,13 @@ describe('FocusContain', () => { ReactDOM.render(, container); expect(document.activeElement).toBe(inputRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(buttonRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(divRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(divRef.current); }); @@ -90,15 +100,15 @@ describe('FocusContain', () => { ReactDOM.render(, container); buttonRef.current.focus(); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(buttonRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(buttonRef.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); // Focus should be restored to the contained area const rAF = window.requestAnimationFrame; @@ -132,15 +142,15 @@ describe('FocusContain', () => { ReactDOM.render(, container); expect(document.activeElement).toBe(buttonRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button4Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); }); @@ -167,13 +177,13 @@ describe('FocusContain', () => { ReactDOM.render(, container); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); }); @@ -213,15 +223,15 @@ describe('FocusContain', () => { ReactDOM.render(, container); buttonRef.current.focus(); expect(document.activeElement).toBe(buttonRef.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabNext(); + tabNext(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button4Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button3Ref.current); - createEventTarget(document.activeElement).tabPrevious(); + tabPrevious(createEventTarget(document.activeElement)); expect(document.activeElement).toBe(button2Ref.current); }); }); diff --git a/packages/react-interactions/accessibility/src/__tests__/FocusGroup-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/FocusGroup-test.internal.js index 9bd4c614987ad..77124b5e208fa 100644 --- a/packages/react-interactions/accessibility/src/__tests__/FocusGroup-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/FocusGroup-test.internal.js @@ -7,7 +7,7 @@ * @flow */ -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library'; +import {createEventTarget} from 'react-interactions/events/src/dom/event-testing-library'; import {emulateBrowserTab} from '../shared/emulateBrowserTab'; let React; diff --git a/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js index b3c2910ea965c..bfad6d9ae5c3a 100644 --- a/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js @@ -7,7 +7,7 @@ * @flow */ -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library'; +import {createEventTarget} from 'react-interactions/events/src/dom/event-testing-library'; import {emulateBrowserTab} from '../shared/emulateBrowserTab'; let React; diff --git a/packages/react-interactions/accessibility/src/shared/emulateBrowserTab.js b/packages/react-interactions/accessibility/src/shared/emulateBrowserTab.js index c726ad246a62e..6b34509b722b5 100644 --- a/packages/react-interactions/accessibility/src/shared/emulateBrowserTab.js +++ b/packages/react-interactions/accessibility/src/shared/emulateBrowserTab.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library'; +import {createEventTarget} from 'react-interactions/events/src/dom/event-testing-library'; // This function is used by the a11y modules for testing export function emulateBrowserTab(backwards: boolean): void { diff --git a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js index 71414f3021907..2530300230b4a 100644 --- a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js @@ -14,7 +14,7 @@ import { createEventTarget, platform, setPointerEvent, -} from '../testing-library'; +} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js index b07c94076dea5..e8c2aeb639488 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js @@ -9,7 +9,11 @@ 'use strict'; -import {createEventTarget, setPointerEvent, platform} from '../testing-library'; +import { + createEventTarget, + setPointerEvent, + platform, +} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js index 51a31cd953195..9a971ee8391d1 100644 --- a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget, setPointerEvent} from '../testing-library'; +import {createEventTarget, setPointerEvent} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js index 7c19e9d42aa73..bdea4c4044566 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget, setPointerEvent} from '../testing-library'; +import {createEventTarget, setPointerEvent} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js index 152dcb770180f..e81472252dca0 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js @@ -14,7 +14,7 @@ let ReactFeatureFlags; let ReactDOM; let useKeyboard; -import {createEventTarget} from '../testing-library'; +import {createEventTarget} from '../event-testing-library'; function initializeModules(hasPointerEvents) { jest.resetModules(); diff --git a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js index 3d20efb3f1dc4..22ac74d4c627e 100644 --- a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget} from '../testing-library'; +import {createEventTarget} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js index 9fd4b0f3139c8..bcc6557bd3331 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js @@ -14,8 +14,9 @@ import { buttonsType, createEventTarget, describeWithPointerEvent, + resetActivePointers, setPointerEvent, -} from '../testing-library'; +} from '../event-testing-library'; let React; let ReactFeatureFlags; @@ -47,6 +48,7 @@ describeWithPointerEvent('Press responder', hasPointerEvents => { ReactDOM.render(null, container); document.body.removeChild(container); container = null; + resetActivePointers(); }); describe('disabled', () => { diff --git a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js index f9d96b8757d9d..399e063e474b5 100644 --- a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js @@ -13,8 +13,9 @@ import { buttonType, buttonsType, createEventTarget, + resetActivePointers, setPointerEvent, -} from '../testing-library'; +} from '../event-testing-library'; let React; let ReactFeatureFlags; @@ -59,6 +60,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { ReactDOM.render(null, container); document.body.removeChild(container); container = null; + resetActivePointers(); }); describe('disabled', () => { diff --git a/packages/react-interactions/events/src/dom/__tests__/Scroll-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Scroll-test.internal.js index ccb6f6093df45..c3ccac571513c 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Scroll-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Scroll-test.internal.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget, setPointerEvent} from '../testing-library'; +import {createEventTarget, setPointerEvent} from '../event-testing-library'; let React; let ReactFeatureFlags; diff --git a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js index 9b5fce004f1e2..2e14164504ae7 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js @@ -16,7 +16,8 @@ import { describeWithPointerEvent, setPointerEvent, testWithPointerType, -} from '../testing-library'; + resetActivePointers, +} from '../event-testing-library'; let React; let ReactFeatureFlags; @@ -82,6 +83,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { ReactDOM.render(null, container); document.body.removeChild(container); container = null; + resetActivePointers(); }); test('supports repeated use', () => { @@ -295,12 +297,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { const buttons = buttonsType.primary; target.pointerdown({button, buttons, pointerId: 1, pointerType}); expect(onTapStart).toHaveBeenCalledTimes(1); - if (hasPointerEvents) { - target.pointerdown({button, buttons, pointerId: 2, pointerType}); - } else { - // TouchEvents - target.pointerdown([{pointerId: 1}, {pointerId: 2}]); - } + target.pointerdown({button, buttons, pointerId: 2, pointerType}); expect(onTapStart).toHaveBeenCalledTimes(1); }); @@ -313,28 +310,28 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { buttons: buttonsType.secondary, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // middle-click target.pointerdown({ button: buttonType.auxiliary, buttons: buttonsType.auxiliary, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // virtual middle-click with misleading 'buttons' value target.pointerdown({ button: buttonType.auxiliary, buttons: 0, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // pen eraser target.pointerdown({ button: buttonType.eraser, buttons: buttonsType.eraser, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); } // alt-click target.pointerdown({ @@ -343,7 +340,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { altKey: true, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // ctrl-click target.pointerdown({ button: buttonType.primary, @@ -351,7 +348,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { ctrlKey: true, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // meta-click target.pointerdown({ button: buttonType.primary, @@ -359,7 +356,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { metaKey: true, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); // shift-click target.pointerdown({ button: buttonType.primary, @@ -367,7 +364,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { shiftKey: true, pointerType, }); - target.pointerup(); + target.pointerup({pointerType}); expect(onTapStart).toHaveBeenCalledTimes(0); }); @@ -565,8 +562,10 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { testWithPointerType('requires activation', pointerType => { const target = createEventTarget(ref.current); target.setBoundingClientRect(rect); - target.pointerhover({pointerType, ...coordinates}); - target.pointermove({pointerType, ...coordinates}); + if (pointerType !== 'touch') { + target.pointerhover({pointerType, ...coordinates}); + target.pointermove({pointerType, ...coordinates}); + } expect(onTapUpdate).not.toBeCalled(); }); @@ -771,12 +770,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { const button = buttonType.primary; const buttons = buttonsType.primary; target.pointerdown({button, buttons, pointerId: 1, pointerType}); - if (hasPointerEvents) { - target.pointerdown({button, buttons, pointerId: 2, pointerType}); - } else { - // TouchEvents - target.pointerdown([{pointerId: 1}, {pointerId: 2}]); - } + target.pointerdown({button, buttons, pointerId: 2, pointerType}); expect(onTapCancel).toHaveBeenCalledTimes(1); }); diff --git a/packages/react-interactions/events/src/dom/event-testing-library/constants.js b/packages/react-interactions/events/src/dom/event-testing-library/constants.js new file mode 100644 index 0000000000000..c131e5fc1c2b0 --- /dev/null +++ b/packages/react-interactions/events/src/dom/event-testing-library/constants.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +export const defaultPointerId = 1; +export const defaultPointerSize = 23; +export const defaultBrowserChromeSize = 50; + +/** + * Button property + * This property only guarantees to indicate which buttons are pressed during events caused by pressing or + * releasing one or multiple buttons. As such, it is not reliable for events such as 'mouseenter', 'mouseleave', + * 'mouseover', 'mouseout' or 'mousemove'. Furthermore, the semantics differ for PointerEvent, where the value + * for 'pointermove' will always be -1. + */ + +export const buttonType = { + // no change since last event + none: -1, + // left-mouse + // touch contact + // pen contact + primary: 0, + // right-mouse + // pen barrel button + secondary: 2, + // middle mouse + auxiliary: 1, + // back mouse + back: 3, + // forward mouse + forward: 4, + // pen eraser + eraser: 5, +}; + +/** + * Buttons bitmask + */ + +export const buttonsType = { + none: 0, + // left-mouse + // touch contact + // pen contact + primary: 1, + // right-mouse + // pen barrel button + secondary: 2, + // middle mouse + auxiliary: 4, + // back mouse + back: 8, + // forward mouse + forward: 16, + // pen eraser + eraser: 32, +}; diff --git a/packages/react-interactions/events/src/dom/testing-library/domEnvironment.js b/packages/react-interactions/events/src/dom/event-testing-library/domEnvironment.js similarity index 59% rename from packages/react-interactions/events/src/dom/testing-library/domEnvironment.js rename to packages/react-interactions/events/src/dom/event-testing-library/domEnvironment.js index 2c87c783e7cc5..eb08170826ec8 100644 --- a/packages/react-interactions/events/src/dom/testing-library/domEnvironment.js +++ b/packages/react-interactions/events/src/dom/event-testing-library/domEnvironment.js @@ -65,54 +65,3 @@ export const platform = { } }, }; - -/** - * Button property - * This property only guarantees to indicate which buttons are pressed during events caused by pressing or - * releasing one or multiple buttons. As such, it is not reliable for events such as 'mouseenter', 'mouseleave', - * 'mouseover', 'mouseout' or 'mousemove'. Furthermore, the semantics differ for PointerEvent, where the value - * for 'pointermove' will always be -1. - */ - -export const buttonType = { - // no change since last event - none: -1, - // left-mouse - // touch contact - // pen contact - primary: 0, - // right-mouse - // pen barrel button - secondary: 2, - // middle mouse - auxiliary: 1, - // back mouse - back: 3, - // forward mouse - forward: 4, - // pen eraser - eraser: 5, -}; - -/** - * Buttons bitmask - */ - -export const buttonsType = { - none: 0, - // left-mouse - // touch contact - // pen contact - primary: 1, - // right-mouse - // pen barrel button - secondary: 2, - // middle mouse - auxiliary: 4, - // back mouse - back: 8, - // forward mouse - forward: 16, - // pen eraser - eraser: 32, -}; diff --git a/packages/react-interactions/events/src/dom/event-testing-library/domEventSequences.js b/packages/react-interactions/events/src/dom/event-testing-library/domEventSequences.js new file mode 100644 index 0000000000000..f8b063eca01c2 --- /dev/null +++ b/packages/react-interactions/events/src/dom/event-testing-library/domEventSequences.js @@ -0,0 +1,361 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +import { + buttonType, + buttonsType, + defaultPointerId, + defaultPointerSize, + defaultBrowserChromeSize, +} from './constants'; +import * as domEvents from './domEvents'; +import {hasPointerEvent, platform} from './domEnvironment'; +import * as touchStore from './touchStore'; + +/** + * Converts a PointerEvent payload to a Touch + */ +function createTouch(target, payload) { + const { + height = defaultPointerSize, + pageX, + pageY, + pointerId, + pressure = 1, + twist = 0, + width = defaultPointerSize, + x = 0, + y = 0, + } = payload; + + return { + clientX: x, + clientY: y, + force: pressure, + identifier: pointerId, + pageX: pageX || x, + pageY: pageY || y, + radiusX: width / 2, + radiusY: height / 2, + rotationAngle: twist, + target, + screenX: x, + screenY: y + defaultBrowserChromeSize, + }; +} + +/** + * Converts a PointerEvent to a TouchEvent + */ +function createTouchEventPayload(target, touch, payload) { + const { + altKey = false, + ctrlKey = false, + metaKey = false, + preventDefault, + shiftKey = false, + timeStamp, + } = payload; + + return { + altKey, + changedTouches: [touch], + ctrlKey, + metaKey, + preventDefault, + shiftKey, + targetTouches: touchStore.getTargetTouches(target), + timeStamp, + touches: touchStore.getTouches(), + }; +} + +function getPointerType(payload) { + let pointerType = 'mouse'; + if (payload != null && payload.pointerType != null) { + pointerType = payload.pointerType; + } + return pointerType; +} + +/** + * Pointer events sequences. + * + * Creates representative browser event sequences for high-level gestures based on pointers. + * This allows unit tests to be written in terms of simple pointer interactions while testing + * that the responses to those interactions account for the complex sequence of events that + * browsers produce as a result. + * + * Every time a new pointer touches the surface a 'touchstart' event should be dispatched. + * - 'changedTouches' contains the new touch. + * - 'targetTouches' contains all the active pointers for the target. + * - 'touches' contains all the active pointers on the surface. + * + * Every time an existing pointer moves a 'touchmove' event should be dispatched. + * - 'changedTouches' contains the updated touch. + * + * Every time an existing pointer leaves the surface a 'touchend' event should be dispatched. + * - 'changedTouches' contains the released touch. + * - 'targetTouches' contains any of the remaining active pointers for the target. + */ + +export function contextmenu( + target, + defaultPayload, + {pointerType = 'mouse', modified} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + + const payload = { + pointerId: defaultPointerId, + pointerType, + ...defaultPayload, + }; + + const preventDefault = payload.preventDefault; + + if (pointerType === 'touch') { + if (hasPointerEvent()) { + dispatch( + domEvents.pointerdown({ + ...payload, + button: buttonType.primary, + buttons: buttonsType.primary, + }), + ); + } + const touch = createTouch(target, payload); + touchStore.addTouch(touch); + const touchEventPayload = createTouchEventPayload(target, touch, payload); + dispatch(domEvents.touchstart(touchEventPayload)); + dispatch( + domEvents.contextmenu({ + button: buttonType.primary, + buttons: buttonsType.none, + preventDefault, + }), + ); + touchStore.removeTouch(touch); + } else if (pointerType === 'mouse') { + if (modified === true) { + const button = buttonType.primary; + const buttons = buttonsType.primary; + const ctrlKey = true; + if (hasPointerEvent()) { + dispatch( + domEvents.pointerdown({button, buttons, ctrlKey, pointerType}), + ); + } + dispatch(domEvents.mousedown({button, buttons, ctrlKey})); + if (platform.get() === 'mac') { + dispatch( + domEvents.contextmenu({button, buttons, ctrlKey, preventDefault}), + ); + } + } else { + const button = buttonType.secondary; + const buttons = buttonsType.secondary; + if (hasPointerEvent()) { + dispatch(domEvents.pointerdown({button, buttons, pointerType})); + } + dispatch(domEvents.mousedown({button, buttons})); + dispatch(domEvents.contextmenu({button, buttons, preventDefault})); + } + } +} + +export function pointercancel(target, defaultPayload) { + const dispatchEvent = arg => target.dispatchEvent(arg); + const pointerType = getPointerType(defaultPayload); + + const payload = { + pointerId: defaultPointerId, + pointerType, + ...defaultPayload, + }; + + if (hasPointerEvent()) { + dispatchEvent(domEvents.pointercancel(payload)); + } else { + if (pointerType === 'mouse') { + dispatchEvent(domEvents.dragstart(payload)); + } else { + const touch = createTouch(target, payload); + touchStore.removeTouch(touch); + const touchEventPayload = createTouchEventPayload(target, touch, payload); + dispatchEvent(domEvents.touchcancel(touchEventPayload)); + } + } +} + +export function pointerdown(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + const pointerType = getPointerType(defaultPayload); + + const payload = { + button: buttonType.primary, + buttons: buttonsType.primary, + pointerId: defaultPointerId, + pointerType, + ...defaultPayload, + }; + + if (pointerType === 'mouse') { + if (hasPointerEvent()) { + dispatch(domEvents.pointerover(payload)); + dispatch(domEvents.pointerenter(payload)); + } + dispatch(domEvents.mouseover(payload)); + dispatch(domEvents.mouseenter(payload)); + if (hasPointerEvent()) { + dispatch(domEvents.pointerdown(payload)); + } + dispatch(domEvents.mousedown(payload)); + if (document.activeElement !== target) { + dispatch(domEvents.focus()); + } + } else { + if (hasPointerEvent()) { + dispatch(domEvents.pointerover(payload)); + dispatch(domEvents.pointerenter(payload)); + dispatch(domEvents.pointerdown(payload)); + } + const touch = createTouch(target, payload); + touchStore.addTouch(touch); + const touchEventPayload = createTouchEventPayload(target, touch, payload); + dispatch(domEvents.touchstart(touchEventPayload)); + if (hasPointerEvent()) { + dispatch(domEvents.gotpointercapture(payload)); + } + } +} + +export function pointerenter(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + + const payload = { + pointerId: defaultPointerId, + ...defaultPayload, + }; + + if (hasPointerEvent()) { + dispatch(domEvents.pointerover(payload)); + dispatch(domEvents.pointerenter(payload)); + } + dispatch(domEvents.mouseover(payload)); + dispatch(domEvents.mouseenter(payload)); +} + +export function pointerexit(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + + const payload = { + pointerId: defaultPointerId, + ...defaultPayload, + }; + + if (hasPointerEvent()) { + dispatch(domEvents.pointerout(payload)); + dispatch(domEvents.pointerleave(payload)); + } + dispatch(domEvents.mouseout(payload)); + dispatch(domEvents.mouseleave(payload)); +} + +export function pointerhover(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + + const payload = { + pointerId: defaultPointerId, + ...defaultPayload, + }; + + if (hasPointerEvent()) { + dispatch(domEvents.pointermove(payload)); + } + dispatch(domEvents.mousemove(payload)); +} + +export function pointermove(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + const pointerType = getPointerType(defaultPayload); + + const payload = { + pointerId: defaultPointerId, + pointerType, + ...defaultPayload, + }; + + if (hasPointerEvent()) { + dispatch( + domEvents.pointermove({ + pressure: pointerType === 'touch' ? 1 : 0.5, + ...payload, + }), + ); + } else { + if (pointerType === 'mouse') { + dispatch(domEvents.mousemove(payload)); + } else { + const touch = createTouch(target, payload); + touchStore.updateTouch(touch); + const touchEventPayload = createTouchEventPayload(target, touch, payload); + dispatch(domEvents.touchmove(touchEventPayload)); + } + } +} + +export function pointerup(target, defaultPayload) { + const dispatch = arg => target.dispatchEvent(arg); + const pointerType = getPointerType(defaultPayload); + + const payload = { + pointerId: defaultPointerId, + pointerType, + ...defaultPayload, + }; + + if (pointerType === 'mouse') { + if (hasPointerEvent()) { + dispatch(domEvents.pointerup(payload)); + } + dispatch(domEvents.mouseup(payload)); + dispatch(domEvents.click(payload)); + } else { + if (hasPointerEvent()) { + dispatch(domEvents.pointerup(payload)); + dispatch(domEvents.lostpointercapture(payload)); + dispatch(domEvents.pointerout(payload)); + dispatch(domEvents.pointerleave(payload)); + } + const touch = createTouch(target, payload); + touchStore.removeTouch(touch); + const touchEventPayload = createTouchEventPayload(target, touch, payload); + dispatch(domEvents.touchend(touchEventPayload)); + dispatch(domEvents.mouseover(payload)); + dispatch(domEvents.mousemove(payload)); + dispatch(domEvents.mousedown(payload)); + if (document.activeElement !== target) { + dispatch(domEvents.focus()); + } + dispatch(domEvents.mouseup(payload)); + dispatch(domEvents.click(payload)); + } +} + +/** + * This function should be called after each test to ensure the touchStore is cleared + * in cases where the mock pointers weren't released before the test completed + * (e.g., a test failed or ran a partial gesture). + */ +export function resetActivePointers() { + touchStore.clear(); +} diff --git a/packages/react-interactions/events/src/dom/testing-library/domEvents.js b/packages/react-interactions/events/src/dom/event-testing-library/domEvents.js similarity index 83% rename from packages/react-interactions/events/src/dom/testing-library/domEvents.js rename to packages/react-interactions/events/src/dom/event-testing-library/domEvents.js index 19605a1ad64a5..f62a6a5622e1e 100644 --- a/packages/react-interactions/events/src/dom/testing-library/domEvents.js +++ b/packages/react-interactions/events/src/dom/event-testing-library/domEvents.js @@ -9,7 +9,12 @@ 'use strict'; -import {buttonType, buttonsType} from './domEnvironment'; +import { + buttonType, + buttonsType, + defaultPointerSize, + defaultBrowserChromeSize, +} from './constants'; /** * Native event object mocks for higher-level events. @@ -26,9 +31,6 @@ import {buttonType, buttonsType} from './domEnvironment'; * 3. PointerEvent and TouchEvent fields are normalized (e.g., 'rotationAngle' -> 'twist') */ -const defaultPointerSize = 23; -const defaultBrowserChromeSize = 50; - function emptyFunction() {} function createEvent(type, data = {}) { @@ -77,7 +79,7 @@ function createPointerEvent( offsetY = 0, pageX, pageY, - pointerId = 1, + pointerId, pressure = 0, preventDefault = emptyFunction, pointerType = 'mouse', @@ -215,80 +217,12 @@ function createMouseEvent( } function createTouchEvent(type, payload) { - const touchesPayload = Array.isArray(payload) ? payload : [payload]; - const firstTouch = touchesPayload[0]; - let altKey = false; - let ctrlKey = false; - let metaKey = false; - let preventDefault = emptyFunction; - let shiftKey = false; - let timeStamp; - - if (firstTouch != null) { - if (firstTouch.altKey != null) { - altKey = firstTouch.altKey; - } - if (firstTouch.ctrlKey != null) { - ctrlKey = firstTouch.ctrlKey; - } - if (firstTouch.metaKey != null) { - metaKey = firstTouch.metaKey; - } - if (firstTouch.preventDefault != null) { - preventDefault = firstTouch.preventDefault; - } - if (firstTouch.shiftKey != null) { - shiftKey = firstTouch.shiftKey; - } - if (firstTouch.timeStamp != null) { - timeStamp = firstTouch.timeStamp; - } - } - - const touches = touchesPayload.map( - ({ - height = defaultPointerSize, - pageX, - pageY, - pointerId = 1, - pressure = 1, - twist = 0, - width = defaultPointerSize, - x = 0, - y = 0, - } = {}) => { - return { - clientX: x, - clientY: y, - force: pressure, - identifier: pointerId, - pageX: pageX || x, - pageY: pageY || y, - radiusX: width / 2, - radiusY: height / 2, - rotationAngle: twist, - screenX: x, - screenY: y + defaultBrowserChromeSize, - }; - }, - ); - - const activeTouches = type !== 'touchend' ? touches : []; - return createEvent(type, { - altKey, - changedTouches: touches, - ctrlKey, + ...payload, detail: 0, - metaKey, - preventDefault, - shiftKey, sourceCapabilities: { firesTouchEvents: true, }, - targetTouches: activeTouches, - timeStamp, - touches: activeTouches, }); } diff --git a/packages/react-interactions/events/src/dom/testing-library/index.js b/packages/react-interactions/events/src/dom/event-testing-library/index.js similarity index 70% rename from packages/react-interactions/events/src/dom/testing-library/index.js rename to packages/react-interactions/events/src/dom/event-testing-library/index.js index f76e815b8b609..81b4c701e674c 100644 --- a/packages/react-interactions/events/src/dom/testing-library/index.js +++ b/packages/react-interactions/events/src/dom/event-testing-library/index.js @@ -9,15 +9,11 @@ 'use strict'; +import {buttonType, buttonsType} from './constants'; import * as domEvents from './domEvents'; import * as domEventSequences from './domEventSequences'; -import { - buttonType, - buttonsType, - hasPointerEvent, - setPointerEvent, - platform, -} from './domEnvironment'; +import {hasPointerEvent, setPointerEvent, platform} from './domEnvironment'; +import {describeWithPointerEvent, testWithPointerType} from './testHelpers'; const createEventTarget = node => ({ node, @@ -52,32 +48,6 @@ const createEventTarget = node => ({ virtualclick(payload) { node.dispatchEvent(domEvents.virtualclick(payload)); }, - tabNext() { - node.dispatchEvent( - domEvents.keydown({ - key: 'Tab', - }), - ); - node.dispatchEvent( - domEvents.keyup({ - key: 'Tab', - }), - ); - }, - tabPrevious() { - node.dispatchEvent( - domEvents.keydown({ - key: 'Tab', - shiftKey: true, - }), - ); - node.dispatchEvent( - domEvents.keyup({ - key: 'Tab', - shiftKey: true, - }), - ); - }, /** * PointerEvent abstraction. * Dispatches the expected sequence of PointerEvents, MouseEvents, and @@ -137,28 +107,7 @@ const createEventTarget = node => ({ }, }); -function describeWithPointerEvent(message, describeFn) { - const pointerEvent = 'PointerEvent'; - const fallback = 'MouseEvent/TouchEvent'; - describe.each` - value | name - ${true} | ${pointerEvent} - ${false} | ${fallback} - `(`${message}: $name`, entry => { - const hasPointerEvents = entry.value; - setPointerEvent(hasPointerEvents); - describeFn(hasPointerEvents); - }); -} - -function testWithPointerType(message, testFn) { - const table = hasPointerEvent() - ? ['mouse', 'touch', 'pen'] - : ['mouse', 'touch']; - test.each(table)(`${message}: %s`, pointerType => { - testFn(pointerType); - }); -} +const resetActivePointers = domEventSequences.resetActivePointers; export { buttonType, @@ -167,6 +116,7 @@ export { describeWithPointerEvent, platform, hasPointerEvent, + resetActivePointers, setPointerEvent, testWithPointerType, }; diff --git a/packages/react-interactions/events/src/dom/event-testing-library/testHelpers.js b/packages/react-interactions/events/src/dom/event-testing-library/testHelpers.js new file mode 100644 index 0000000000000..1e1058bdb97ce --- /dev/null +++ b/packages/react-interactions/events/src/dom/event-testing-library/testHelpers.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +import {hasPointerEvent, setPointerEvent} from './domEnvironment'; + +export function describeWithPointerEvent(message, describeFn) { + const pointerEvent = 'PointerEvent'; + const fallback = 'MouseEvent/TouchEvent'; + describe.each` + value | name + ${true} | ${pointerEvent} + ${false} | ${fallback} + `(`${message}: $name`, entry => { + const hasPointerEvents = entry.value; + setPointerEvent(hasPointerEvents); + describeFn(hasPointerEvents); + }); +} + +export function testWithPointerType(message, testFn) { + const table = hasPointerEvent() + ? ['mouse', 'touch', 'pen'] + : ['mouse', 'touch']; + test.each(table)(`${message}: %s`, pointerType => { + testFn(pointerType); + }); +} diff --git a/packages/react-interactions/events/src/dom/event-testing-library/touchStore.js b/packages/react-interactions/events/src/dom/event-testing-library/touchStore.js new file mode 100644 index 0000000000000..25fc03dd6bbfb --- /dev/null +++ b/packages/react-interactions/events/src/dom/event-testing-library/touchStore.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +/** + * Touch events state machine. + * + * Keeps track of the active pointers and allows them to be reflected in touch events. + */ + +const activeTouches = new Map(); + +export function addTouch(touch) { + const identifier = touch.identifier; + const target = touch.target; + if (!activeTouches.has(target)) { + activeTouches.set(target, new Map()); + } + if (activeTouches.get(target).get(identifier)) { + // Do not allow existing touches to be overwritten + // eslint-disable-next-line react-internal/no-production-logging + console.error( + 'Touch with identifier %s already exists. Did not record touch start.', + identifier, + ); + } else { + activeTouches.get(target).set(identifier, touch); + } +} + +export function updateTouch(touch) { + const identifier = touch.identifier; + const target = touch.target; + if (activeTouches.get(target) != null) { + activeTouches.get(target).set(identifier, touch); + } else { + // eslint-disable-next-line react-internal/no-production-logging + console.error( + 'Touch with identifier %s does not exist. Cannot record touch move without a touch start.', + identifier, + ); + } +} + +export function removeTouch(touch) { + const identifier = touch.identifier; + const target = touch.target; + if (activeTouches.get(target) != null) { + if (activeTouches.get(target).has(identifier)) { + activeTouches.get(target).delete(identifier); + } else { + // eslint-disable-next-line react-internal/no-production-logging + console.error( + 'Touch with identifier %s does not exist. Cannot record touch end without a touch start.', + identifier, + ); + } + } +} + +export function getTouches() { + const touches = []; + activeTouches.forEach((_, target) => { + touches.push(...getTargetTouches(target)); + }); + return touches; +} + +export function getTargetTouches(target) { + if (activeTouches.get(target) != null) { + return Array.from(activeTouches.get(target).values()); + } + return []; +} + +export function clear() { + activeTouches.clear(); +} diff --git a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js b/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js deleted file mode 100644 index d6da485f4bfd1..0000000000000 --- a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -import * as domEvents from './domEvents'; -import { - buttonType, - buttonsType, - hasPointerEvent, - platform, -} from './domEnvironment'; - -function emptyFunction() {} - -function getPointerType(payload) { - let pointerType = 'mouse'; - if (payload != null && payload.pointerType != null) { - pointerType = payload.pointerType; - } else if (Array.isArray(payload)) { - pointerType = 'touch'; - } - return pointerType; -} - -export function contextmenu( - target, - {preventDefault = emptyFunction} = {}, - {pointerType = 'mouse', modified} = {}, -) { - const dispatch = arg => target.dispatchEvent(arg); - if (pointerType === 'touch') { - if (hasPointerEvent()) { - dispatch( - domEvents.pointerdown({ - button: buttonType.primary, - buttons: buttonsType.primary, - pointerType, - }), - ); - } - dispatch(domEvents.touchstart()); - dispatch( - domEvents.contextmenu({ - button: buttonType.primary, - buttons: buttonsType.none, - preventDefault, - }), - ); - } else if (pointerType === 'mouse') { - if (modified === true) { - const button = buttonType.primary; - const buttons = buttonsType.primary; - const ctrlKey = true; - if (hasPointerEvent()) { - dispatch( - domEvents.pointerdown({button, buttons, ctrlKey, pointerType}), - ); - } - dispatch(domEvents.mousedown({button, buttons, ctrlKey})); - if (platform.get() === 'mac') { - dispatch( - domEvents.contextmenu({button, buttons, ctrlKey, preventDefault}), - ); - } - } else { - const button = buttonType.secondary; - const buttons = buttonsType.secondary; - if (hasPointerEvent()) { - dispatch(domEvents.pointerdown({button, buttons, pointerType})); - } - dispatch(domEvents.mousedown({button, buttons})); - dispatch(domEvents.contextmenu({button, buttons, preventDefault})); - } - } -} - -export function pointercancel(target, payload) { - const dispatchEvent = arg => target.dispatchEvent(arg); - const pointerType = getPointerType(payload); - if (hasPointerEvent()) { - dispatchEvent(domEvents.pointercancel(payload)); - } else { - if (pointerType === 'mouse') { - dispatchEvent(domEvents.dragstart(payload)); - } else { - dispatchEvent(domEvents.touchcancel(payload)); - } - } -} - -export function pointerdown(target, defaultPayload) { - const dispatch = arg => target.dispatchEvent(arg); - const pointerType = getPointerType(defaultPayload); - - if (Array.isArray(defaultPayload)) { - // Arrays are for multi-touch only - dispatch(domEvents.touchstart(defaultPayload)); - } else { - const payload = { - button: buttonType.primary, - buttons: buttonsType.primary, - ...defaultPayload, - }; - if (pointerType === 'mouse') { - if (hasPointerEvent()) { - dispatch(domEvents.pointerover(payload)); - dispatch(domEvents.pointerenter(payload)); - } - dispatch(domEvents.mouseover(payload)); - dispatch(domEvents.mouseenter(payload)); - if (hasPointerEvent()) { - dispatch(domEvents.pointerdown(payload)); - } - dispatch(domEvents.mousedown(payload)); - if (document.activeElement !== target) { - dispatch(domEvents.focus()); - } - } else { - if (hasPointerEvent()) { - dispatch(domEvents.pointerover(payload)); - dispatch(domEvents.pointerenter(payload)); - dispatch(domEvents.pointerdown(payload)); - } - dispatch(domEvents.touchstart(payload)); - if (hasPointerEvent()) { - dispatch(domEvents.gotpointercapture(payload)); - } - } - } -} - -export function pointerenter(target, payload) { - const dispatch = arg => target.dispatchEvent(arg); - if (hasPointerEvent()) { - dispatch(domEvents.pointerover(payload)); - dispatch(domEvents.pointerenter(payload)); - } - dispatch(domEvents.mouseover(payload)); - dispatch(domEvents.mouseenter(payload)); -} - -export function pointerexit(target, payload) { - const dispatch = arg => target.dispatchEvent(arg); - if (hasPointerEvent()) { - dispatch(domEvents.pointerout(payload)); - dispatch(domEvents.pointerleave(payload)); - } - dispatch(domEvents.mouseout(payload)); - dispatch(domEvents.mouseleave(payload)); -} - -export function pointerhover(target, payload) { - const dispatch = arg => target.dispatchEvent(arg); - if (hasPointerEvent()) { - dispatch(domEvents.pointermove(payload)); - } - dispatch(domEvents.mousemove(payload)); -} - -export function pointermove(target, payload) { - const dispatch = arg => target.dispatchEvent(arg); - const pointerType = getPointerType(payload); - if (hasPointerEvent()) { - dispatch( - domEvents.pointermove({ - pressure: pointerType === 'touch' ? 1 : 0.5, - ...payload, - }), - ); - } else { - if (pointerType === 'mouse') { - dispatch(domEvents.mousemove(payload)); - } else { - dispatch(domEvents.touchmove(payload)); - } - } -} - -export function pointerup(target, payload) { - const dispatch = arg => target.dispatchEvent(arg); - const pointerType = getPointerType(payload); - - if (Array.isArray(payload)) { - // Arrays are for multi-touch only - dispatch(domEvents.touchend(payload)); - } else if (pointerType === 'mouse') { - if (hasPointerEvent()) { - dispatch(domEvents.pointerup(payload)); - } - dispatch(domEvents.mouseup(payload)); - dispatch(domEvents.click(payload)); - } else { - if (hasPointerEvent()) { - dispatch(domEvents.pointerup(payload)); - dispatch(domEvents.lostpointercapture(payload)); - dispatch(domEvents.pointerout(payload)); - dispatch(domEvents.pointerleave(payload)); - } - dispatch(domEvents.touchend(payload)); - dispatch(domEvents.mouseover(payload)); - dispatch(domEvents.mousemove(payload)); - dispatch(domEvents.mousedown(payload)); - if (document.activeElement !== target) { - dispatch(domEvents.focus()); - } - dispatch(domEvents.mouseup(payload)); - dispatch(domEvents.click(payload)); - } -} diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js index 14fa5cccc01c1..5a25b0b065de6 100644 --- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -9,7 +9,7 @@ 'use strict'; -import {createEventTarget} from 'react-interactions/events/src/dom/testing-library'; +import {createEventTarget} from 'react-interactions/events/src/dom/event-testing-library'; let React; let ReactFeatureFlags;