[react-interactions] Event testing library improvements (#17614)

Introduces a state machine around pointer events to produce more accurate mock
touch events. This allows multi-touch unit tests to be written entirely in
terms of mock pointer interactions, while producing the expected
'changedTouches', 'targetTouches', and 'touches' fields for mock touch events.
This commit is contained in:
Nicolas Gallagher
2019-12-18 23:32:48 +00:00
committed by GitHub
parent 95056b6836
commit a5e951d4cc
23 changed files with 631 additions and 456 deletions
@@ -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(<Test />, 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(<Test />, 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(<Test />, 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(<Test />, 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(<Test />, 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);
});
});
@@ -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;
@@ -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;
@@ -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 {
@@ -14,7 +14,7 @@ import {
createEventTarget,
platform,
setPointerEvent,
} from '../testing-library';
} from '../event-testing-library';
let React;
let ReactFeatureFlags;
@@ -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;
@@ -9,7 +9,7 @@
'use strict';
import {createEventTarget, setPointerEvent} from '../testing-library';
import {createEventTarget, setPointerEvent} from '../event-testing-library';
let React;
let ReactFeatureFlags;
@@ -9,7 +9,7 @@
'use strict';
import {createEventTarget, setPointerEvent} from '../testing-library';
import {createEventTarget, setPointerEvent} from '../event-testing-library';
let React;
let ReactFeatureFlags;
@@ -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();
@@ -9,7 +9,7 @@
'use strict';
import {createEventTarget} from '../testing-library';
import {createEventTarget} from '../event-testing-library';
let React;
let ReactFeatureFlags;
@@ -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', () => {
@@ -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', () => {
@@ -9,7 +9,7 @@
'use strict';
import {createEventTarget, setPointerEvent} from '../testing-library';
import {createEventTarget, setPointerEvent} from '../event-testing-library';
let React;
let ReactFeatureFlags;
@@ -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);
});
@@ -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,
};
@@ -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,
};
@@ -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();
}
@@ -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,
});
}
@@ -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,
};
@@ -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);
});
}
@@ -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();
}
@@ -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));
}
}
@@ -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;