Add dom-event-testing-libary as a private package

This is copied from facebook/react with various fixes applied (which I'll push
upstream at a later date). Necessary for testing the Responder Event System
rewrite.
This commit is contained in:
Nicolas Gallagher
2020-03-26 18:17:02 -07:00
parent fa6e269832
commit fe013b30dc
13 changed files with 2008 additions and 0 deletions
@@ -0,0 +1,156 @@
# `dom-event-testing-library`
A library for unit testing high-level interactions via simple pointer events, e.g.,
`pointerdown`, that produce realistic and complete DOM event sequences.
There are number of challenges involved in unit testing modules that work with
DOM events.
1. Testing environments with and without support for the `PointerEvent` API.
2. Testing various user interaction modes including mouse, touch, and pen use.
3. Testing against the event sequences browsers actually produce (e.g., emulated
touch and mouse events.)
4. Testing against the event properties DOM events include (i.e., more complete
mock data)
4. Testing against "virtual" events produced by tools like screen-readers.
Writing unit tests to cover all these scenarios is tedious and error prone. This
event testing library is designed to avoid these issues by allowing developers to
more easily dispatch events in unit tests, and to more reliably test interactions
while using an API based on `PointerEvent`.
## Example
```js
import {
describeWithPointerEvent,
testWithPointerType,
clearPointers,
createEventTarget,
setPointerEvent,
} from 'dom-event-testing-library';
describeWithPointerEvent('useTap', hasPointerEvent => {
beforeEach(() => {
// basic PointerEvent mock
setPointerEvent(hasPointerEvent);
});
afterEach(() => {
// clear active pointers between test runs
clearPointers();
});
// test all the pointer types supported by the environment
testWithPointerType('pointer down', pointerType => {
const ref = createRef(null);
const onTapStart = jest.fn();
render(() => {
useTap(ref, { onTapStart });
return <div ref={ref} />
});
// create an event target
const target = createEventTarget(ref.current);
// dispatch high-level pointer event
target.pointerdown({ pointerType });
expect(onTapStart).toBeCalled();
});
});
```
This tests the interaction in multiple scenarios. In each case, a realistic DOM
event sequencewith complete mock eventsis produced. When running in a mock
environment without the `PointerEvent` API, the test runs for both `mouse` and
`touch` pointer types. When `touch` is the pointer type it produces emulated mouse
events. When running in a mock environment with the `PointerEvent` API, the test
runs for `mouse`, `touch`, and `pen` pointer types.
It's important to cover all these scenarios because it's very easy to introduce
bugs e.g., double calling of callbacks if not accounting for emulated mouse
events, differences in target capturing between `touch` and `mouse` pointers, and
the different semantics of `button` across event APIs.
Default values are provided for the expected native events properties. They can
also be customized as needed in a test.
```js
target.pointerdown({
button: 0,
buttons: 1,
pageX: 10,
pageY: 10,
pointerType,
// NOTE: use x,y instead of clientX,clientY
x: 10,
y: 10
});
```
Tests that dispatch multiple pointer events will dispatch multi-touch native events
on the target.
```js
// first pointer is active
target.pointerdown({pointerId: 1, pointerType});
// second pointer is active
target.pointerdown({pointerId: 2, pointerType});
```
## API
### Target and events
To create a new event target pass the DOM node to `createEventTarget(node)`. This target can then be used to dispatch event sequences and customize the event payload. The following are currently supported:
* `blur`
* `click`
* `contextmenu`
* `focus`
* `keydown`
* `keyup`
* `pointercancel`
* `pointerdown`
* `pointerhover` (moves when pointer is not down)
* `pointermove` (moves when pointer is down)
* `pointerover`
* `pointerout`
* `scroll`
* `select`
* `selectionchange`
* `tap` (equivalent to `pointerdown` followed by `pointerup`)
* `virtualclick`
The target also has `node` property equal to the node that was used to create the target, and a `setBoundClientRect({x,y,width,height})` method that can be used to mock the return value of `getBoundingClientRect`.
### Jest helpers
#### `describeWithPointerEvent`
This is just like `describe` but it will run the entire test suite twice, once in an environment with `PointerEvent` mocked and once without.
```js
describeWithPointerEvent('useTap', hasPointerEvent => {
// test suite
});
```
#### `testWithPointerType`
The is just like `test` but it will run the test for every pointer type supported by the environment. When `PointerEvent` is mocked, the pointer types will be `mouse`, `touch`, and `pen`; otherwise the pointer types will be `mouse` and `touch`.
```js
testWithPointerType('pointer down', pointerType => {
// test unit
});
```
### jsdom environment helpers
#### platform
Interactions that account for Windows / macOS differences can change the platform by calling `platform.set(value)`, where `value` can be either `'mac'` or `'windows'`. To retreive the current platform call `platform.get()`, and the clear it call `platform.clear()`.
#### hasPointerEvent / setPointerEvent
Interactions implemented using `PointerEvent` can create a basic mock for jsdom by calling `setPointerEvent(true)` (disable with `setPointerEvent(false)`), and check whether `PointerEvent` is available by calling `hasPointerEvent()`.
@@ -0,0 +1 @@
export * from './src/index';
@@ -0,0 +1,10 @@
{
"private": true,
"name": "dom-event-testing-library",
"version": "0.0.0",
"main": "index.js",
"description": "Browser event sequences for unit tests",
"author": "Nicolas Gallagher",
"license": "MIT",
"homepage": "https://github.com/necolas/react-native-web/tree/master/packages/dom-event-testing-library"
}
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`describeWithPointerEvent: MouseEvent/TouchEvent provides boolean to tests 1`] = `false`;
exports[`describeWithPointerEvent: MouseEvent/TouchEvent testWithPointerType: mouse 1`] = `"mouse"`;
exports[`describeWithPointerEvent: MouseEvent/TouchEvent testWithPointerType: touch 1`] = `"touch"`;
exports[`describeWithPointerEvent: PointerEvent provides boolean to tests 1`] = `true`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: mouse 1`] = `"mouse"`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: pen 1`] = `"pen"`;
exports[`describeWithPointerEvent: PointerEvent testWithPointerType: touch 1`] = `"touch"`;
@@ -0,0 +1,365 @@
/* eslint-env jasmine, jest */
/**
* 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.
*/
'use strict';
import { defaultBrowserChromeSize } from '../constants';
import {
clearPointers,
createEventTarget,
describeWithPointerEvent,
testWithPointerType
} from '../index';
/**
* Unit test helpers
*/
describeWithPointerEvent('describeWithPointerEvent', pointerEvent => {
test('provides boolean to tests', () => {
expect(pointerEvent).toMatchSnapshot();
});
testWithPointerType('testWithPointerType', pointerType => {
expect(pointerType).toMatchSnapshot();
});
});
/**
* createEventTarget
*/
describe('createEventTarget', () => {
let node;
beforeEach(() => {
node = document.createElement('div');
});
afterEach(() => {
node = null;
clearPointers();
});
test('returns expected API', () => {
const target = createEventTarget(node);
expect(target.node).toEqual(node);
expect(Object.keys(target)).toMatchInlineSnapshot(`
Array [
"node",
"blur",
"click",
"focus",
"keydown",
"keyup",
"scroll",
"select",
"selectionchange",
"virtualclick",
"contextmenu",
"pointercancel",
"pointerdown",
"pointerhover",
"pointermove",
"pointerover",
"pointerout",
"pointerup",
"tap",
"setBoundingClientRect",
]
`);
});
/**
* Simple events
*/
describe('.blur()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('blur', e => {
expect(e.relatedTarget).toBeUndefined();
});
target.blur();
});
test('custom payload', () => {
const target = createEventTarget(node);
const relatedTarget = document.createElement('div');
node.addEventListener('blur', e => {
expect(e.relatedTarget).toBe(relatedTarget);
});
target.blur({ relatedTarget });
});
});
describe('.click()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(false);
expect(e.button).toEqual(0);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(false);
expect(e.detail).toEqual(1);
expect(typeof e.getModifierState).toEqual('function');
expect(e.metaKey).toEqual(false);
expect(e.movementX).toEqual(0);
expect(e.movementY).toEqual(0);
expect(e.offsetX).toEqual(0);
expect(e.offsetY).toEqual(0);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(typeof e.preventDefault).toEqual('function');
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(defaultBrowserChromeSize);
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.click();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(true);
expect(e.button).toEqual(1);
expect(e.buttons).toEqual(4);
expect(e.clientX).toEqual(10);
expect(e.clientY).toEqual(20);
expect(e.ctrlKey).toEqual(true);
expect(e.metaKey).toEqual(true);
expect(e.movementX).toEqual(1);
expect(e.movementY).toEqual(2);
expect(e.offsetX).toEqual(5);
expect(e.offsetY).toEqual(5);
expect(e.pageX).toEqual(50);
expect(e.pageY).toEqual(50);
expect(e.screenX).toEqual(10);
expect(e.screenY).toEqual(20 + defaultBrowserChromeSize);
expect(e.shiftKey).toEqual(true);
});
target.click({
altKey: true,
button: 1,
buttons: 4,
x: 10,
y: 20,
ctrlKey: true,
metaKey: true,
movementX: 1,
movementY: 2,
offsetX: 5,
offsetY: 5,
pageX: 50,
pageY: 50,
shiftKey: true
});
});
});
describe('.focus()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('focus', e => {
expect(e.relatedTarget).toBeUndefined();
});
target.focus();
});
test('custom payload', () => {
const target = createEventTarget(node);
const relatedTarget = document.createElement('div');
node.addEventListener('focus', e => {
expect(e.relatedTarget).toBe(relatedTarget);
});
target.focus({ relatedTarget });
});
});
describe('.keydown()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('keydown', e => {
expect(e.altKey).toEqual(false);
expect(e.ctrlKey).toEqual(false);
expect(typeof e.getModifierState).toEqual('function');
expect(e.key).toEqual('');
expect(e.metaKey).toEqual(false);
expect(typeof e.preventDefault).toEqual('function');
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.keydown();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('keydown', e => {
expect(e.altKey).toEqual(true);
expect(e.ctrlKey).toEqual(true);
expect(e.isComposing).toEqual(true);
expect(e.key).toEqual('Enter');
expect(e.metaKey).toEqual(true);
expect(e.shiftKey).toEqual(true);
});
target.keydown({
altKey: true,
ctrlKey: true,
isComposing: true,
key: 'Enter',
metaKey: true,
shiftKey: true
});
});
});
describe('.keyup()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('keyup', e => {
expect(e.altKey).toEqual(false);
expect(e.ctrlKey).toEqual(false);
expect(typeof e.getModifierState).toEqual('function');
expect(e.key).toEqual('');
expect(e.metaKey).toEqual(false);
expect(typeof e.preventDefault).toEqual('function');
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.keydown();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('keyup', e => {
expect(e.altKey).toEqual(true);
expect(e.ctrlKey).toEqual(true);
expect(e.isComposing).toEqual(true);
expect(e.key).toEqual('Enter');
expect(e.metaKey).toEqual(true);
expect(e.shiftKey).toEqual(true);
});
target.keyup({
altKey: true,
ctrlKey: true,
isComposing: true,
key: 'Enter',
metaKey: true,
shiftKey: true
});
});
});
describe('.scroll()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('scroll', e => {
expect(e.type).toEqual('scroll');
});
target.scroll();
});
});
describe('.virtualclick()', () => {
test('default', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
expect(e.altKey).toEqual(false);
expect(e.button).toEqual(0);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(false);
expect(e.detail).toEqual(0);
expect(typeof e.getModifierState).toEqual('function');
expect(e.metaKey).toEqual(false);
expect(e.movementX).toEqual(0);
expect(e.movementY).toEqual(0);
expect(e.offsetX).toEqual(0);
expect(e.offsetY).toEqual(0);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(typeof e.preventDefault).toEqual('function');
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(0);
expect(e.shiftKey).toEqual(false);
expect(typeof e.timeStamp).toEqual('number');
});
target.virtualclick();
});
test('custom payload', () => {
const target = createEventTarget(node);
node.addEventListener('click', e => {
// expect most of the custom payload to be ignored
expect(e.altKey).toEqual(true);
expect(e.button).toEqual(1);
expect(e.buttons).toEqual(0);
expect(e.clientX).toEqual(0);
expect(e.clientY).toEqual(0);
expect(e.ctrlKey).toEqual(true);
expect(e.detail).toEqual(0);
expect(e.metaKey).toEqual(true);
expect(e.pageX).toEqual(0);
expect(e.pageY).toEqual(0);
expect(e.screenX).toEqual(0);
expect(e.screenY).toEqual(0);
expect(e.shiftKey).toEqual(true);
});
target.virtualclick({
altKey: true,
button: 1,
buttons: 4,
x: 10,
y: 20,
ctrlKey: true,
metaKey: true,
pageX: 50,
pageY: 50,
shiftKey: true
});
});
});
/**
* Complex event sequences
*
* ...coming soon
*/
/**
* Other APIs
*/
test('.setBoundingClientRect()', () => {
const target = createEventTarget(node);
expect(node.getBoundingClientRect()).toMatchInlineSnapshot(`
Object {
"bottom": 0,
"height": 0,
"left": 0,
"right": 0,
"top": 0,
"width": 0,
}
`);
target.setBoundingClientRect({ x: 10, y: 20, width: 100, height: 200 });
expect(node.getBoundingClientRect()).toMatchInlineSnapshot(`
Object {
"bottom": 220,
"height": 200,
"left": 10,
"right": 110,
"top": 20,
"width": 100,
}
`);
});
});
@@ -0,0 +1,63 @@
/**
* 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.
*/
'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
};
@@ -0,0 +1,268 @@
/**
* 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.
*/
'use strict';
const defaultConfig = {
constructorType: 'Event',
defaultInit: { bubbles: true, cancelable: true, composed: true }
};
const eventConfigs = {
// Focus Events
blur: {
constructorType: 'FocusEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
focus: {
constructorType: 'FocusEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
focusin: {
constructorType: 'FocusEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
focusout: {
constructorType: 'FocusEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
// Keyboard Events
keydown: {
constructorType: 'KeyboardEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
keyup: {
constructorType: 'KeyboardEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Mouse Events
click: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
contextmenu: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
dblclick: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
drag: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
dragend: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
dragenter: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
dragexit: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
dragleave: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
dragover: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
dragstart: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
drop: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
mousedown: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
mouseenter: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
mouseleave: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
mousemove: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
mouseout: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
mouseover: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
mouseup: {
constructorType: 'MouseEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Selection events
select: {
constructorType: 'Event',
defaultInit: { bubbles: true, cancelable: false }
},
// Touch events
touchcancel: {
constructorType: 'TouchEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
touchend: {
constructorType: 'TouchEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
touchmove: {
constructorType: 'TouchEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
touchstart: {
constructorType: 'TouchEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Pointer events
gotpointercapture: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
lostpointercapture: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: false, cancelable: false, composed: true }
},
pointercancel: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
pointerdown: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
pointerenter: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: false, cancelable: false }
},
pointerleave: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: false, cancelable: false }
},
pointermove: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
pointerout: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
pointerover: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
pointerup: {
constructorType: 'PointerEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Image events
error: {
constructorType: 'Event',
defaultInit: { bubbles: false, cancelable: false }
},
load: {
constructorType: 'UIEvent',
defaultInit: { bubbles: false, cancelable: false }
},
// Form Events
change: {
constructorType: 'Event',
defaultInit: { bubbles: true, cancelable: false }
},
input: {
constructorType: 'InputEvent',
defaultInit: { bubbles: true, cancelable: false, composed: true }
},
invalid: {
constructorType: 'Event',
defaultInit: { bubbles: false, cancelable: true }
},
submit: {
constructorType: 'Event',
defaultInit: { bubbles: true, cancelable: true }
},
reset: {
constructorType: 'Event',
defaultInit: { bubbles: true, cancelable: true }
},
// Clipboard Events
copy: {
constructorType: 'ClipboardEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
cut: {
constructorType: 'ClipboardEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
paste: {
constructorType: 'ClipboardEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Composition Events
compositionend: {
constructorType: 'CompositionEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
compositionstart: {
constructorType: 'CompositionEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
compositionupdate: {
constructorType: 'CompositionEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
},
// Other events
scroll: {
constructorType: 'UIEvent',
defaultInit: { bubbles: false, cancelable: false }
},
wheel: {
constructorType: 'WheelEvent',
defaultInit: { bubbles: true, cancelable: true, composed: true }
}
};
function getEventConfig(type) {
return eventConfigs[type] || defaultConfig;
}
export default function createEvent(type, init) {
const config = getEventConfig(type);
const { constructorType, defaultInit } = config;
const eventInit = { ...init, ...defaultInit };
const event = document.createEvent(constructorType);
const { bubbles, cancelable, ...data } = eventInit;
event.initEvent(type, bubbles, cancelable);
if (data != null) {
Object.keys(data).forEach(key => {
const value = data[key];
if (key === 'timeStamp' && !value) {
return;
}
Object.defineProperty(event, key, { value });
});
}
return event;
}
@@ -0,0 +1,67 @@
/* eslint-env jasmine, jest */
/**
* 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.
*/
'use strict';
/**
* Change environment support for PointerEvent.
*/
const emptyFunction = function() {};
export function hasPointerEvent() {
return global != null && global.PointerEvent != null;
}
export function setPointerEvent(bool) {
const pointerCaptureFn = name => id => {
if (typeof id !== 'number') {
if (process.env.NODE_DEV !== 'production') {
console.error('A pointerId must be passed to "%s"', name);
}
}
};
global.PointerEvent = bool ? emptyFunction : undefined;
global.HTMLElement.prototype.setPointerCapture = bool
? pointerCaptureFn('setPointerCapture')
: undefined;
global.HTMLElement.prototype.releasePointerCapture = bool
? pointerCaptureFn('releasePointerCapture')
: undefined;
}
/**
* Change environment host platform.
*/
const platformGetter = jest.spyOn(global.navigator, 'platform', 'get');
export const platform = {
clear() {
platformGetter.mockClear();
},
get() {
return global.navigator.platform === 'MacIntel' ? 'mac' : 'windows';
},
set(name) {
switch (name) {
case 'mac': {
platformGetter.mockReturnValue('MacIntel');
break;
}
case 'windows': {
platformGetter.mockReturnValue('Win32');
break;
}
default: {
break;
}
}
}
};
@@ -0,0 +1,382 @@
/**
* 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.
*/
'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) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const {
ctrlKey,
// eslint-disable-next-line
pointerType: _,
...restPayload
} = defaultPayload;
const payload = {
pointerId: defaultPointerId,
...restPayload,
button: buttonType.primary,
buttons: buttonsType.primary,
pointerType
};
const preventDefault = payload.preventDefault;
if (pointerType === 'touch') {
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown(payload));
}
const touch = createTouch(target, payload);
touchStore.addTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchstart(touchEventPayload));
dispatch(
domEvents.mousemove({
...payload,
button: buttonType.primary,
buttons: buttonsType.none
})
);
dispatch(
domEvents.contextmenu({
...payload,
button: buttonType.primary,
buttons: buttonsType.none,
preventDefault
})
);
touchStore.removeTouch(touch);
} else if (pointerType === 'mouse') {
if (ctrlKey === true) {
const { button, buttons } = payload;
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown({ ...payload, ctrlKey }));
}
dispatch(domEvents.mousedown({ ...payload, 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({ ...payload, button, buttons }));
}
dispatch(domEvents.mousedown({ ...payload, button, buttons }));
dispatch(domEvents.contextmenu({ ...payload, button, buttons, preventDefault }));
}
}
}
export function focus(target, defaultPayload = {}) {
const dispatch = arg => target.dispatchEvent(arg);
const { relatedTarget, ...payload } = defaultPayload;
if (relatedTarget) {
relatedTarget.dispatchEvent(domEvents.focusout({ ...payload, relatedTarget: target }));
}
dispatch(domEvents.focusin({ ...payload, relatedTarget }));
if (relatedTarget) {
relatedTarget.dispatchEvent(domEvents.blur({ ...payload, relatedTarget: target }));
}
dispatch(domEvents.focus({ ...payload, relatedTarget }));
}
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));
focus(target);
} 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 pointerover(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 pointerout(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));
}
// pointer is not down while moving
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));
}
// pointer is down while moving
export function pointermove(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.pointermove({ pressure: 0.5, button: -1, ...payload }));
}
dispatch(domEvents.mousemove(payload));
} else {
if (hasPointerEvent()) {
dispatch(
domEvents.pointermove({
pressure: 1,
button: -1,
...payload
})
);
}
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);
const isGesture = touchStore.removeTouch(touch);
const touchEventPayload = createTouchEventPayload(target, touch, payload);
dispatch(domEvents.touchend(touchEventPayload));
// emulated mouse events don't occur for multi-touch or after 'touchmove'
if (!isGesture) {
dispatch(domEvents.mouseover(payload));
dispatch(domEvents.mousemove(payload));
dispatch(domEvents.mousedown(payload));
}
focus(target);
if (!isGesture) {
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 clearPointers() {
touchStore.clear();
}
@@ -0,0 +1,438 @@
/**
* 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.
*/
'use strict';
import createEvent from './createEvent';
import { buttonType, buttonsType, defaultPointerSize, defaultBrowserChromeSize } from './constants';
/**
* Native event object mocks for higher-level events.
*
* 1. Each event type defines the exact object that it accepts. This ensures
* that no arbitrary properties can be assigned to events, and the properties
* that don't exist on specific event types (e.g., 'pointerType') are not added
* to the respective native event.
*
* 2. Properties that cannot be relied on due to inconsistent browser support (e.g., 'x' and 'y') are not
* added to the native event. Others that shouldn't be arbitrarily customized (e.g., 'screenX')
* are automatically inferred from associated values.
*
* 3. PointerEvent and TouchEvent fields are normalized (e.g., 'rotationAngle' -> 'twist')
*/
function emptyFunction() {}
function createGetModifierState(keyArg, data) {
if (keyArg === 'Alt') {
return data.altKey || false;
}
if (keyArg === 'Control') {
return data.ctrlKey || false;
}
if (keyArg === 'Meta') {
return data.metaKey || false;
}
if (keyArg === 'Shift') {
return data.shiftKey || false;
}
}
/**
* KeyboardEvent
*/
function createKeyboardEvent(
type,
{
altKey = false,
ctrlKey = false,
isComposing = false,
key = '',
metaKey = false,
preventDefault = emptyFunction,
shiftKey = false
} = {}
) {
const modifierState = { altKey, ctrlKey, metaKey, shiftKey };
const eventPayload = {
altKey,
ctrlKey,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
isComposing,
key,
metaKey,
preventDefault,
shiftKey
};
if (isComposing) {
eventPayload.keyCode = 229;
}
return createEvent(type, eventPayload);
}
/**
* MouseEvent
*/
function createMouseEvent(
type,
{
altKey = false,
button = buttonType.none,
buttons = buttonsType.none,
ctrlKey = false,
detail = 1,
metaKey = false,
movementX = 0,
movementY = 0,
offsetX = 0,
offsetY = 0,
pageX,
pageY,
preventDefault = emptyFunction,
screenX,
screenY,
shiftKey = false,
timeStamp,
x = 0,
y = 0
} = {}
) {
const modifierState = { altKey, ctrlKey, metaKey, shiftKey };
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
ctrlKey,
detail,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
metaKey,
movementX,
movementY,
offsetX,
offsetY,
pageX: pageX || x,
pageY: pageY || y,
preventDefault,
screenX: screenX === 0 ? screenX : x,
screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize,
shiftKey,
timeStamp
});
}
/**
* PointerEvent
*/
function createPointerEvent(
type,
{
altKey = false,
button = buttonType.none,
buttons = buttonsType.none,
ctrlKey = false,
detail = 1,
height,
metaKey = false,
movementX = 0,
movementY = 0,
offsetX = 0,
offsetY = 0,
pageX,
pageY,
pointerId,
pressure = 0,
preventDefault = emptyFunction,
pointerType = 'mouse',
screenX,
screenY,
shiftKey = false,
tangentialPressure = 0,
tiltX = 0,
tiltY = 0,
timeStamp,
twist = 0,
width,
x = 0,
y = 0
} = {}
) {
const modifierState = { altKey, ctrlKey, metaKey, shiftKey };
const isMouse = pointerType === 'mouse';
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
ctrlKey,
detail,
getModifierState(keyArg) {
return createGetModifierState(keyArg, modifierState);
},
height: isMouse ? 1 : height != null ? height : defaultPointerSize,
metaKey,
movementX,
movementY,
offsetX,
offsetY,
pageX: pageX || x,
pageY: pageY || y,
pointerId,
pointerType,
pressure,
preventDefault,
releasePointerCapture: emptyFunction,
screenX: screenX === 0 ? screenX : x,
screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize,
setPointerCapture: emptyFunction,
shiftKey,
tangentialPressure,
tiltX,
tiltY,
timeStamp,
twist,
width: isMouse ? 1 : width != null ? width : defaultPointerSize
});
}
/**
* TouchEvent
*/
function createTouchEvent(type, payload) {
return createEvent(type, {
preventDefault: emptyFunction,
...payload,
detail: 0,
sourceCapabilities: {
firesTouchEvents: true
}
});
}
/**
* DOM events
*/
export function blur({ relatedTarget } = {}) {
return createEvent('blur', { relatedTarget });
}
export function click(payload) {
return createMouseEvent('click', {
button: buttonType.primary,
...payload
});
}
export function contextmenu(payload) {
return createMouseEvent('contextmenu', {
...payload,
detail: 0
});
}
export function dragstart(payload) {
return createMouseEvent('dragstart', {
...payload,
detail: 0
});
}
export function focus({ relatedTarget } = {}) {
return createEvent('focus', { relatedTarget });
}
export function focusin({ relatedTarget } = {}) {
return createEvent('focusin', { relatedTarget });
}
export function focusout({ relatedTarget } = {}) {
return createEvent('focusout', { relatedTarget });
}
export function gotpointercapture(payload) {
return createPointerEvent('gotpointercapture', payload);
}
export function keydown(payload) {
return createKeyboardEvent('keydown', payload);
}
export function keyup(payload) {
return createKeyboardEvent('keyup', payload);
}
export function lostpointercapture(payload) {
return createPointerEvent('lostpointercapture', payload);
}
export function mousedown(payload) {
// The value of 'button' and 'buttons' for 'mousedown' must not be none.
const button =
payload != null && payload.button !== buttonType.none ? payload.button : buttonType.primary;
const buttons =
payload != null && payload.buttons !== buttonsType.none ? payload.buttons : buttonsType.primary;
return createMouseEvent('mousedown', {
...payload,
button,
buttons
});
}
export function mouseenter(payload) {
return createMouseEvent('mouseenter', payload);
}
export function mouseleave(payload) {
return createMouseEvent('mouseleave', payload);
}
export function mousemove(payload) {
return createMouseEvent('mousemove', {
// 0 is also the uninitialized value (i.e., don't assume it means primary button down)
button: 0,
buttons: 0,
...payload
});
}
export function mouseout(payload) {
return createMouseEvent('mouseout', payload);
}
export function mouseover(payload) {
return createMouseEvent('mouseover', payload);
}
export function mouseup(payload) {
return createMouseEvent('mouseup', {
button: buttonType.primary,
...payload,
buttons: buttonsType.none
});
}
export function pointercancel(payload) {
return createPointerEvent('pointercancel', {
...payload,
buttons: 0,
detail: 0,
height: 1,
pageX: 0,
pageY: 0,
pressure: 0,
screenX: 0,
screenY: 0,
width: 1,
x: 0,
y: 0
});
}
export function pointerdown(payload) {
const isTouch = payload != null && payload.pointerType === 'touch';
return createPointerEvent('pointerdown', {
button: buttonType.primary,
buttons: buttonsType.primary,
pressure: isTouch ? 1 : 0.5,
...payload
});
}
export function pointerenter(payload) {
return createPointerEvent('pointerenter', payload);
}
export function pointerleave(payload) {
return createPointerEvent('pointerleave', payload);
}
export function pointermove(payload) {
return createPointerEvent('pointermove', {
...payload,
button: buttonType.none,
buttons: buttonsType.none
});
}
export function pointerout(payload) {
return createPointerEvent('pointerout', payload);
}
export function pointerover(payload) {
return createPointerEvent('pointerover', payload);
}
export function pointerup(payload) {
return createPointerEvent('pointerup', {
button: buttonType.primary,
...payload,
buttons: buttonsType.none,
pressure: 0
});
}
export function scroll() {
return createEvent('scroll', { bubbles: false });
}
export function select() {
return createEvent('select');
}
export function selectionchange() {
return createEvent('selectionchange');
}
export function touchcancel(payload) {
return createTouchEvent('touchcancel', payload);
}
export function touchend(payload) {
return createTouchEvent('touchend', payload);
}
export function touchmove(payload) {
return createTouchEvent('touchmove', payload);
}
export function touchstart(payload) {
return createTouchEvent('touchstart', payload);
}
export function virtualclick(payload) {
return createMouseEvent('click', {
button: 0,
...payload,
buttons: 0,
detail: 0,
height: 1,
pageX: 0,
pageY: 0,
pressure: 0,
screenX: 0,
screenY: 0,
width: 1,
x: 0,
y: 0
});
}
@@ -0,0 +1,126 @@
/**
* 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.
*/
'use strict';
import { buttonType, buttonsType } from './constants';
import * as domEvents from './domEvents';
import * as domEventSequences from './domEventSequences';
import { hasPointerEvent, setPointerEvent, platform } from './domEnvironment';
import { describeWithPointerEvent, testWithPointerType } from './testHelpers';
const createEventTarget = node => ({
node,
/**
* Simple events abstraction.
*/
blur(payload) {
node.dispatchEvent(domEvents.blur(payload));
},
click(payload) {
node.dispatchEvent(domEvents.click(payload));
},
focus(payload) {
domEventSequences.focus(node, payload);
try {
node.focus();
} catch (e) {}
},
keydown(payload) {
node.dispatchEvent(domEvents.keydown(payload));
},
keyup(payload) {
node.dispatchEvent(domEvents.keyup(payload));
},
scroll(payload) {
node.dispatchEvent(domEvents.scroll(payload));
},
select(payload) {
node.dispatchEvent(domEvents.select(payload));
},
// selectionchange is only dispatched on 'document'
selectionchange(payload) {
document.dispatchEvent(domEvents.selectionchange(payload));
},
virtualclick(payload) {
node.dispatchEvent(domEvents.virtualclick(payload));
},
/**
* PointerEvent abstraction.
* Dispatches the expected sequence of PointerEvents, MouseEvents, and
* TouchEvents for a given environment.
*/
contextmenu(payload) {
domEventSequences.contextmenu(node, payload);
},
// node no longer receives events for the pointer
pointercancel(payload) {
domEventSequences.pointercancel(node, payload);
},
// node dispatches down events
pointerdown(payload) {
domEventSequences.pointerdown(node, payload);
},
// node dispatches move events (pointer is not down)
pointerhover(payload) {
domEventSequences.pointerhover(node, payload);
},
// node dispatches move events (pointer is down)
pointermove(payload) {
domEventSequences.pointermove(node, payload);
},
// node dispatches enter & over events
pointerover(payload) {
domEventSequences.pointerover(node, payload);
},
// node dispatches exit & leave events
pointerout(payload) {
domEventSequences.pointerout(node, payload);
},
// node dispatches up events
pointerup(payload) {
domEventSequences.pointerup(node, payload);
},
/**
* Gesture abstractions.
* Helpers for event sequences expected in a gesture.
* target.tap({ pointerType: 'touch' })
*/
tap(payload) {
domEventSequences.pointerdown(payload);
domEventSequences.pointerup(payload);
},
/**
* Utilities
*/
setBoundingClientRect({ x, y, width, height }) {
node.getBoundingClientRect = function() {
return {
width,
height,
left: x,
right: x + width,
top: y,
bottom: y + height
};
};
}
});
const clearPointers = domEventSequences.clearPointers;
export {
buttonType,
buttonsType,
clearPointers,
createEventTarget,
describeWithPointerEvent,
platform,
hasPointerEvent,
setPointerEvent,
testWithPointerType
};
@@ -0,0 +1,33 @@
/* eslint-env jasmine, jest */
/**
* 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.
*/
'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,84 @@
/**
* 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.
*/
'use strict';
/**
* Touch events state machine.
*
* Keeps track of the active pointers and allows them to be reflected in touch events.
*/
let isGesture = false;
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
console.error(
'Touch with identifier %s already exists. Did not record touch start.',
identifier
);
} else {
activeTouches.get(target).set(identifier, touch);
}
isGesture = activeTouches.size > 1;
}
export function updateTouch(touch) {
const identifier = touch.identifier;
const target = touch.target;
if (activeTouches.get(target) != null) {
activeTouches.get(target).set(identifier, touch);
isGesture = true;
} else {
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 {
console.error(
'Touch with identifier %s does not exist. Cannot record touch end without a touch start.',
identifier
);
}
}
return isGesture;
}
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();
}