Skip to content

Commit

Permalink
[react-events] DOM event testing library (#16433)
Browse files Browse the repository at this point in the history
This patch formalizes the mock native events and event sequences used in unit tests.

The `createEventTarget` function returns an object that can be used to dispatch native event sequences on the target without having to manually do so across all the scenarios we need to account for. Unit tests can be written as if we were only working with PointerEvent, but they will dispatch realistic native event sequences based on the execution environment (e.g., is PointerEvent supported?) and pointer type.

```
describe.each(environments)('Suite', (hasPointerEvents) => {
  beforeEach(() => {
    // setup
  });

  test.each(pointerTypes)('Test', (pointerType) => {
    const target = createEventTarget(node);
    target.pointerdown({pointerType});
    expect(callback).toBeCalled();
  });
});
```

Every native event that is dispatched now includes a complete object by default. The properties of the events can be customized. Properties that shouldn't be relied on in responder implementations are excluded from the mock native events to ensure tests will fail. Equivalent properties are normalized across different event types, e.g., 'pointerId' is converted to 'identifier' before a TouchEvent is dispatched.
  • Loading branch information
necolas committed Aug 19, 2019
1 parent e89c19d commit 56d1b0f
Show file tree
Hide file tree
Showing 14 changed files with 1,243 additions and 1,224 deletions.
2 changes: 2 additions & 0 deletions packages/react-events/src/dom/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ const pressResponderImpl = {
const isPressed = state.isPressed;
handleStopPropagation(props, context, nativeEvent);
switch (type) {
// START
case 'pointerdown':
Expand Down Expand Up @@ -632,6 +633,7 @@ const pressResponderImpl = {
const previousPointerType = state.pointerType;
handleStopPropagation(props, context, nativeEvent);
switch (type) {
// MOVE
case 'pointermove':
Expand Down
26 changes: 18 additions & 8 deletions packages/react-events/src/dom/Scroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ type ScrollEvent = {|
y: null | number,
|};

const targetEventTypes = [
'scroll',
'pointerdown',
'touchstart',
'keyup',
'wheel',
];
const rootEventTypes = ['touchcancel', 'touchend'];
const hasPointerEvents =
typeof window !== 'undefined' && window.PointerEvent !== undefined;

const targetEventTypes = hasPointerEvents
? ['scroll', 'pointerdown', 'keyup', 'wheel']
: ['scroll', 'mousedown', 'touchstart', 'keyup', 'wheel'];

const rootEventTypes = hasPointerEvents
? ['pointercancel', 'pointerup']
: ['touchcancel', 'touchend'];

function isFunction(obj): boolean {
return typeof obj === 'function';
Expand Down Expand Up @@ -237,17 +239,23 @@ const scrollResponderImpl = {
state.pointerType = pointerType;
break;
}
case 'mousedown':
case 'wheel': {
state.pointerType = 'mouse';
break;
}
case 'pointerdown': {
state.pointerType = pointerType;
if (pointerType === 'touch' && !state.isTouching) {
state.isTouching = true;
context.addRootEventTypes(rootEventTypes);
}
break;
}
case 'touchstart': {
if (!state.isTouching) {
state.isTouching = true;
state.pointerType = 'touch';
context.addRootEventTypes(rootEventTypes);
}
}
Expand All @@ -262,6 +270,8 @@ const scrollResponderImpl = {
const {type} = event;

switch (type) {
case 'pointercancel':
case 'pointerup':
case 'touchcancel':
case 'touchend': {
if (state.isTouching) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@

'use strict';

import {
dispatchLongPressContextMenu,
dispatchRightClickContextMenu,
dispatchModifiedClickContextMenu,
platform,
setPointerEvent,
} from '../test-utils';
import {createEventTarget, platform, setPointerEvent} from '../testing-library';

let React;
let ReactFeatureFlags;
Expand Down Expand Up @@ -62,7 +56,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchRightClickContextMenu(ref.current, {preventDefault});
const target = createEventTarget(ref.current);
target.contextmenu({preventDefault});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
Expand All @@ -80,7 +75,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchLongPressContextMenu(ref.current, {preventDefault});
const target = createEventTarget(ref.current);
target.contextmenu({preventDefault}, {pointerType: 'touch'});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
Expand All @@ -100,7 +96,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchRightClickContextMenu(ref.current);
const target = createEventTarget(ref.current);
target.contextmenu();
expect(onContextMenu).toHaveBeenCalledTimes(0);
});

Expand All @@ -117,7 +114,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchRightClickContextMenu(ref.current, {preventDefault});
const target = createEventTarget(ref.current);
target.contextmenu({preventDefault});
expect(preventDefault).toHaveBeenCalledTimes(0);
expect(onContextMenu).toHaveBeenCalledTimes(1);
});
Expand All @@ -142,7 +140,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchModifiedClickContextMenu(ref.current);
const target = createEventTarget(ref.current);
target.contextmenu({}, {modified: true});
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
Expand All @@ -169,7 +168,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
};
ReactDOM.render(<Component />, container);

dispatchModifiedClickContextMenu(ref.current);
const target = createEventTarget(ref.current);
target.contextmenu({}, {modified: true});
expect(onContextMenu).toHaveBeenCalledTimes(0);
});
});
Expand Down
125 changes: 64 additions & 61 deletions packages/react-events/src/dom/__tests__/Focus-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,7 @@

'use strict';

import {
blur,
focus,
keydown,
setPointerEvent,
platform,
dispatchPointerDown,
dispatchPointerUp,
} from '../test-utils';
import {createEventTarget, setPointerEvent, platform} from '../testing-library';

let React;
let ReactFeatureFlags;
Expand Down Expand Up @@ -73,9 +65,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});

it('does not call callbacks', () => {
const dispatch = arg => ref.current.dispatchEvent(arg);
dispatch(focus());
dispatch(blur());
const target = createEventTarget(ref.current);
target.focus();
target.blur();
expect(onFocus).not.toBeCalled();
expect(onBlur).not.toBeCalled();
});
Expand All @@ -97,9 +89,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});

it('is called after "blur" event', () => {
const dispatch = arg => ref.current.dispatchEvent(arg);
dispatch(focus());
dispatch(blur());
const target = createEventTarget(ref.current);
target.focus();
target.blur();
expect(onBlur).toHaveBeenCalledTimes(1);
});
});
Expand Down Expand Up @@ -127,29 +119,32 @@ describe.each(table)('Focus responder', hasPointerEvents => {
beforeEach(componentInit);

it('is called after "focus" event', () => {
ref.current.dispatchEvent(focus());
const target = createEventTarget(ref.current);
target.focus();
expect(onFocus).toHaveBeenCalledTimes(1);
});

it('is not called if descendants of target receive focus', () => {
innerRef.current.dispatchEvent(focus());
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocus).not.toBeCalled();
});

it('is called with the correct pointerType: mouse', () => {
const target = ref.current;
dispatchPointerDown(target, {pointerType: 'mouse'});
dispatchPointerUp(target, {pointerType: 'mouse'});
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse'}),
);
});

it('is called with the correct pointerType: touch', () => {
const target = ref.current;
dispatchPointerDown(target, {pointerType: 'touch'});
dispatchPointerUp(target, {pointerType: 'touch'});
const target = createEventTarget(ref.current);
const pointerType = 'touch';
target.pointerdown({pointerType});
target.pointerup({pointerType});
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch'}),
Expand All @@ -158,9 +153,10 @@ describe.each(table)('Focus responder', hasPointerEvents => {

if (hasPointerEvents) {
it('is called with the correct pointerType: pen', () => {
const target = ref.current;
dispatchPointerDown(target, {pointerType: 'pen'});
dispatchPointerUp(target, {pointerType: 'pen'});
const target = createEventTarget(ref.current);
const pointerType = 'pen';
target.pointerdown({pointerType});
target.pointerup({pointerType});
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'pen'}),
Expand All @@ -169,10 +165,9 @@ describe.each(table)('Focus responder', hasPointerEvents => {
}

it('is called with the correct pointerType using a keyboard', () => {
const target = ref.current;
// Keyboard tab
target.dispatchEvent(keydown({key: 'Tab'}));
target.dispatchEvent(focus());
const target = createEventTarget(ref.current);
target.keydown({key: 'Tab'});
target.focus();
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'keyboard'}),
Expand All @@ -184,10 +179,11 @@ describe.each(table)('Focus responder', hasPointerEvents => {
jest.resetModules();
initializeModules();
componentInit();
const target = ref.current;

target.dispatchEvent(keydown({key: 'Tab', altKey: true}));
target.dispatchEvent(focus());
const target = createEventTarget(ref.current);
target.keydown({key: 'Tab', altKey: true});
target.focus();

expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -220,20 +216,20 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});

it('is called after "blur" and "focus" events', () => {
const target = ref.current;
target.dispatchEvent(focus());
const target = createEventTarget(ref.current);
target.focus();
expect(onFocusChange).toHaveBeenCalledTimes(1);
expect(onFocusChange).toHaveBeenCalledWith(true);
target.dispatchEvent(blur());
target.blur();
expect(onFocusChange).toHaveBeenCalledTimes(2);
expect(onFocusChange).toHaveBeenCalledWith(false);
});

it('is not called after "blur" and "focus" events on descendants', () => {
const target = innerRef.current;
target.dispatchEvent(focus());
const target = createEventTarget(innerRef.current);
target.focus();
expect(onFocusChange).toHaveBeenCalledTimes(0);
target.dispatchEvent(blur());
target.blur();
expect(onFocusChange).toHaveBeenCalledTimes(0);
});
});
Expand All @@ -259,48 +255,52 @@ describe.each(table)('Focus responder', hasPointerEvents => {
});

it('is called after "focus" and "blur" if keyboard navigation is active', () => {
const target = ref.current;
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
// use keyboard first
container.dispatchEvent(keydown({key: 'Tab'}));
target.dispatchEvent(focus());
containerTarget.keydown({key: 'Tab'});
target.focus();
expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
target.dispatchEvent(blur({relatedTarget: container}));
target.blur({relatedTarget: container});
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
});

it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
const target = ref.current;
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
// use keyboard first
container.dispatchEvent(keydown({key: 'Tab'}));
target.dispatchEvent(focus());
containerTarget.keydown({key: 'Tab'});
target.focus();
expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
// then use pointer on the target, focus should no longer be visible
dispatchPointerDown(target);
target.pointerdown();
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
// onFocusVisibleChange should not be called again
target.dispatchEvent(blur({relatedTarget: container}));
target.blur({relatedTarget: container});
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
});

it('is not called after "focus" and "blur" events without keyboard', () => {
const target = ref.current;
dispatchPointerDown(target);
dispatchPointerUp(target);
dispatchPointerDown(container);
target.dispatchEvent(blur({relatedTarget: container}));
const target = createEventTarget(ref.current);
const containerTarget = createEventTarget(container);
target.pointerdown();
target.pointerup();
containerTarget.pointerdown();
target.blur({relatedTarget: container});
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
});

it('is not called after "blur" and "focus" events on descendants', () => {
const target = innerRef.current;
container.dispatchEvent(keydown({key: 'Tab'}));
target.dispatchEvent(focus());
const innerTarget = createEventTarget(innerRef.current);
const containerTarget = createEventTarget(container);
containerTarget.keydown({key: 'Tab'});
innerTarget.focus();
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
target.dispatchEvent(blur({relatedTarget: container}));
innerTarget.blur({relatedTarget: container});
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
});
});
Expand Down Expand Up @@ -338,10 +338,13 @@ describe.each(table)('Focus responder', hasPointerEvents => {

ReactDOM.render(<Outer />, container);

outerRef.current.dispatchEvent(focus());
outerRef.current.dispatchEvent(blur());
innerRef.current.dispatchEvent(focus());
innerRef.current.dispatchEvent(blur());
const innerTarget = createEventTarget(innerRef.current);
const outerTarget = createEventTarget(outerRef.current);

outerTarget.focus();
outerTarget.blur();
innerTarget.focus();
innerTarget.blur();
expect(events).toEqual([
'outer: onFocus',
'outer: onFocusChange',
Expand Down
Loading

0 comments on commit 56d1b0f

Please sign in to comment.