From ebd1f5ddb0da7e3626b56614e2b5723debadd839 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 15 Aug 2019 10:28:00 -0700 Subject: [PATCH 01/18] [react-events] Press: improve test coverage (#16397) 1. Run the tests in both an environment without PointerEvent and one with PointerEvent. 2. Improve test coverage to include both mouse and touch pointers. 3. Change 'Press' so that it only listens to either pointer events or fallbacks events. --- packages/react-events/src/dom/Press.js | 65 +- .../src/dom/__tests__/Focus-test.internal.js | 24 +- .../__tests__/FocusWithin-test.internal.js | 12 +- .../src/dom/__tests__/Hover-test.internal.js | 9 +- .../src/dom/__tests__/Press-test.internal.js | 2217 +++++------------ packages/react-events/src/dom/test-utils.js | 233 +- 6 files changed, 858 insertions(+), 1702 deletions(-) diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index aa39d91a35ca8..40251331f94bc 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -95,10 +95,14 @@ type PressEvent = {| shiftKey: boolean, |}; +const hasPointerEvents = + typeof window !== 'undefined' && window.PointerEvent !== undefined; + const isMac = typeof window !== 'undefined' && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false; + const DEFAULT_PRESS_RETENTION_OFFSET = { bottom: 20, top: 20, @@ -106,37 +110,32 @@ const DEFAULT_PRESS_RETENTION_OFFSET = { right: 20, }; -const targetEventTypes = [ - 'keydown_active', - // We need to preventDefault on pointerdown for mouse/pen events - // that are in hit target area but not the element area. - 'pointerdown_active', - 'click_active', -]; -const rootEventTypes = [ - 'click', - 'keyup', - 'pointerup', - 'pointermove', - 'scroll', - 'pointercancel', - // We listen to this here so stopPropagation can - // block other mouseup events used internally - 'mouseup_active', - 'touchend', -]; - -// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events. -if (typeof window !== 'undefined' && window.PointerEvent === undefined) { - targetEventTypes.push('touchstart', 'mousedown'); - rootEventTypes.push( - 'mousemove', - 'touchmove', - 'touchcancel', - // Used as a 'cancel' signal for mouse interactions - 'dragstart', - ); -} +const targetEventTypes = hasPointerEvents + ? [ + 'keydown_active', + // We need to preventDefault on pointerdown for mouse/pen events + // that are in hit target area but not the element area. + 'pointerdown_active', + 'click_active', + ] + : ['keydown_active', 'touchstart', 'mousedown', 'click_active']; + +const rootEventTypes = hasPointerEvents + ? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll'] + : [ + 'click', + 'keyup', + 'scroll', + 'mousemove', + 'touchmove', + 'touchcancel', + // Used as a 'cancel' signal for mouse interactions + 'dragstart', + // We listen to this here so stopPropagation can + // block other mouseup events used internally + 'mouseup_active', + 'touchend', + ]; function isFunction(obj): boolean { return typeof obj === 'function'; @@ -539,7 +538,7 @@ const pressResponderImpl = { } state.shouldPreventClick = false; - if (isPointerEvent || isTouchEvent) { + if (isTouchEvent) { state.ignoreEmulatedMouseEvents = true; } else if (isKeyboardEvent) { // Ignore unrelated key events @@ -676,6 +675,7 @@ const pressResponderImpl = { if (state.isPressWithinResponderRegion) { if (isPressed) { const onPressMove = props.onPressMove; + if (isFunction(onPressMove)) { dispatchEvent( event, @@ -777,6 +777,7 @@ const pressResponderImpl = { ); } } + if (state.isPressWithinResponderRegion && button !== 1) { dispatchEvent( event, diff --git a/packages/react-events/src/dom/__tests__/Focus-test.internal.js b/packages/react-events/src/dom/__tests__/Focus-test.internal.js index a432f64091cfb..316bc9f19dcde 100644 --- a/packages/react-events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Focus-test.internal.js @@ -15,8 +15,8 @@ import { keydown, setPointerEvent, platform, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerDown, + dispatchPointerUp, } from '../test-utils'; let React; @@ -138,8 +138,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is called with the correct pointerType: mouse', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'mouse'}); - dispatchPointerPressRelease(target, {pointerType: 'mouse'}); + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse'}), @@ -148,8 +148,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is called with the correct pointerType: touch', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'touch'}); - dispatchPointerPressRelease(target, {pointerType: 'touch'}); + dispatchPointerDown(target, {pointerType: 'touch'}); + dispatchPointerUp(target, {pointerType: 'touch'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch'}), @@ -159,8 +159,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { if (hasPointerEvents) { it('is called with the correct pointerType: pen', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'pen'}); - dispatchPointerPressRelease(target, {pointerType: 'pen'}); + dispatchPointerDown(target, {pointerType: 'pen'}); + dispatchPointerUp(target, {pointerType: 'pen'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'pen'}), @@ -278,7 +278,7 @@ describe.each(table)('Focus responder', hasPointerEvents => { expect(onFocusVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - dispatchPointerPressDown(target); + dispatchPointerDown(target); expect(onFocusVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again @@ -288,9 +288,9 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is not called after "focus" and "blur" events without keyboard', () => { const target = ref.current; - dispatchPointerPressDown(target); - dispatchPointerPressRelease(target); - dispatchPointerPressDown(container); + dispatchPointerDown(target); + dispatchPointerUp(target); + dispatchPointerDown(container); target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(0); }); diff --git a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js index e2340a21f669f..ff54b6f3838e1 100644 --- a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js @@ -14,8 +14,8 @@ import { focus, keydown, setPointerEvent, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerDown, + dispatchPointerUp, } from '../test-utils'; let React; @@ -203,7 +203,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the next target, focus should no longer be visible - dispatchPointerPressDown(innerTarget2); + dispatchPointerDown(innerTarget2); innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); innerTarget2.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); @@ -215,7 +215,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - dispatchPointerPressDown(innerTarget1); + dispatchPointerDown(innerTarget1); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again @@ -225,8 +225,8 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { it('is not called after "focus" and "blur" events without keyboard', () => { const innerTarget = innerRef.current; - dispatchPointerPressDown(innerTarget); - dispatchPointerPressRelease(innerTarget); + dispatchPointerDown(innerTarget); + dispatchPointerUp(innerTarget); innerTarget.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0); }); diff --git a/packages/react-events/src/dom/__tests__/Hover-test.internal.js b/packages/react-events/src/dom/__tests__/Hover-test.internal.js index eb9eab90c3029..2198e74551156 100644 --- a/packages/react-events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Hover-test.internal.js @@ -213,7 +213,8 @@ describe.each(table)('Hover responder', hasPointerEvents => { const target = ref.current; dispatchPointerHoverEnter(target); - dispatchPointerHoverMove(target, {from: {x: 0, y: 0}, to: {x: 1, y: 1}}); + dispatchPointerHoverMove(target, {x: 0, y: 0}); + dispatchPointerHoverMove(target, {x: 1, y: 1}); expect(onHoverMove).toHaveBeenCalledTimes(2); expect(onHoverMove).toHaveBeenCalledWith( expect.objectContaining({type: 'hovermove'}), @@ -317,10 +318,8 @@ describe.each(table)('Hover responder', hasPointerEvents => { const target = ref.current; dispatchPointerHoverEnter(target, {x: 10, y: 10}); - dispatchPointerHoverMove(target, { - from: {x: 10, y: 10}, - to: {x: 20, y: 20}, - }); + dispatchPointerHoverMove(target, {x: 10, y: 10}); + dispatchPointerHoverMove(target, {x: 20, y: 20}); dispatchPointerHoverExit(target, {x: 20, y: 20}); expect(eventLog).toEqual([ diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index de87adde0dc53..a6a42a5d011a3 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -9,6 +9,21 @@ 'use strict'; +import { + click, + dispatchPointerCancel, + dispatchPointerDown, + dispatchPointerUp, + dispatchPointerHoverMove, + dispatchPointerMove, + keydown, + keyup, + scroll, + pointerdown, + pointerup, + setPointerEvent, +} from '../test-utils'; + let React; let ReactFeatureFlags; let ReactDOM; @@ -16,39 +31,9 @@ let PressResponder; let usePressResponder; let Scheduler; -const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; -}; - -function createTouchEvent(type, id, data) { - return createEvent(type, { - changedTouches: [ - { - ...data, - identifier: id, - }, - ], - targetTouches: [ - { - ...data, - identifier: id, - }, - ], - }); -} - -const createKeyboardEvent = (type, data) => { - return createEvent(type, data); -}; - -function init() { +function initializeModules(hasPointerEvents) { + jest.resetModules(); + setPointerEvent(hasPointerEvents); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableFlareAPI = true; React = require('react'); @@ -58,12 +43,23 @@ function init() { Scheduler = require('scheduler'); } -describe('Event responder: Press', () => { +function removePressMoveStrings(eventString) { + if (eventString === 'onPressMove') { + return false; + } + return true; +} + +const forcePointerEvents = true; +const environmentTable = [[forcePointerEvents], [!forcePointerEvents]]; + +const pointerTypesTable = [['mouse'], ['touch']]; + +describe.each(environmentTable)('Press responder', hasPointerEvents => { let container; beforeEach(() => { - jest.resetModules(); - init(); + initializeModules(hasPointerEvents); container = document.createElement('div'); document.body.appendChild(container); }); @@ -92,11 +88,13 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('prevents custom events being dispatched', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointerup')); + it('does not call callbacks', () => { + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target); expect(onPressStart).not.toBeCalled(); expect(onPress).not.toBeCalled(); expect(onPressEnd).not.toBeCalled(); @@ -116,27 +114,22 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerdown" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'pressstart'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer down: %s', + pointerType => { + dispatchPointerDown(ref.current, {pointerType}); + expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressstart'}), + ); + }, + ); - it('is called after auxillary-button "pointerdown" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {button: 1, pointerType: 'mouse'}), - ); + it('is called after auxillary-button pointer down', () => { + dispatchPointerDown(ref.current, {button: 1, pointerType: 'mouse'}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -148,64 +141,38 @@ describe('Event responder: Press', () => { }); it('is not called after "pointermove" following auxillary-button press', () => { - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent( - createEvent('pointerdown', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - button: 1, - pointerType: 'mouse', - clientX: 110, - clientY: 110, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - }); - - it('ignores browser emulated events', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('touchstart')); - ref.current.dispatchEvent(createEvent('mousedown')); + dispatchPointerDown(target, { + button: 1, + pointerType: 'mouse', + }); + dispatchPointerUp(target, { + button: 1, + pointerType: 'mouse', + }); + dispatchPointerHoverMove(target, {x: 110, y: 110}); + dispatchPointerHoverMove(target, {x: 50, y: 50}); expect(onPressStart).toHaveBeenCalledTimes(1); }); it('ignores any events not caused by primary/auxillary-click or touch/pen contact', () => { - ref.current.dispatchEvent(createEvent('pointerdown', {button: 5})); - ref.current.dispatchEvent(createEvent('mousedown', {button: 2})); + const target = ref.current; + dispatchPointerDown(target, {button: 2}); + dispatchPointerDown(target, {button: 5}); expect(onPressStart).toHaveBeenCalledTimes(0); }); it('is called once after "keydown" events for Enter', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keydown({key: 'Enter'})); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), @@ -213,14 +180,11 @@ describe('Event responder: Press', () => { }); it('is called once after "keydown" events for Spacebar', () => { + const target = ref.current; const preventDefault = jest.fn(); - ref.current.dispatchEvent( - createKeyboardEvent('keydown', {key: ' ', preventDefault}), - ); + target.dispatchEvent(keydown({key: ' ', preventDefault})); expect(preventDefault).toBeCalled(); - ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); + target.dispatchEvent(keydown({key: ' ', preventDefault})); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -231,34 +195,9 @@ describe('Event responder: Press', () => { }); it('is not called after "keydown" for other keys', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'a'})); + ref.current.dispatchEvent(keydown({key: 'a'})); expect(onPressStart).not.toBeCalled(); }); - - // No PointerEvent fallbacks - it('is called after "mousedown" event', () => { - ref.current.dispatchEvent( - createEvent('mousedown', { - button: 0, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}), - ); - }); - - it('is called after "touchstart" event', () => { - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressstart'}), - ); - }); }); describe('onPressEnd', () => { @@ -274,36 +213,26 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent(createEvent('pointerup', {pointerType: 'pen'})); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'pressend'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerUp(target, {pointerType}); + expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressend'}), + ); + }, + ); - it('is called after auxillary-button "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {button: 1, pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {button: 1, pointerType: 'mouse'}), - ); + it('is called after auxillary-button pointer up', () => { + const target = ref.current; + dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); + dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({ @@ -314,37 +243,12 @@ describe('Event responder: Press', () => { ); }); - it('ignores browser emulated events', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent(createEvent('mousedown')); - ref.current.dispatchEvent(createEvent('mouseup')); - ref.current.dispatchEvent(createEvent('click')); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressend'}), - ); - }); - it('is called after "keyup" event for Enter', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); // click occurs before keyup - ref.current.dispatchEvent(createKeyboardEvent('click')); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + target.dispatchEvent(click()); + target.dispatchEvent(keyup({key: 'Enter'})); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -352,8 +256,9 @@ describe('Event responder: Press', () => { }); it('is called after "keyup" event for Spacebar', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '})); + const target = ref.current; + target.dispatchEvent(keydown({key: ' '})); + target.dispatchEvent(keyup({key: ' '})); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -361,15 +266,17 @@ describe('Event responder: Press', () => { }); it('is not called after "keyup" event for other keys', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'a'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'a'})); expect(onPressEnd).not.toBeCalled(); }); it('is called with keyboard modifiers', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent( - createKeyboardEvent('keyup', { + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent( + keyup({ key: 'Enter', metaKey: true, ctrlKey: true, @@ -388,41 +295,6 @@ describe('Event responder: Press', () => { }), ); }); - - // No PointerEvent fallbacks - it('is called after "mouseup" event', () => { - ref.current.dispatchEvent( - createEvent('mousedown', { - button: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('mouseup', { - button: 0, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressend'}), - ); - }); - it('is called after "touchend" event', () => { - document.elementFromPoint = () => ref.current; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressend'}), - ); - }); }); describe('onPressChange', () => { @@ -438,22 +310,27 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerdown" and "pointerup" events', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - expect(onPressChange).toHaveBeenCalledTimes(1); - expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createEvent('pointerup')); - expect(onPressChange).toHaveBeenCalledTimes(2); - expect(onPressChange).toHaveBeenCalledWith(false); - }); + it.each(pointerTypesTable)( + 'is called after pointer down and up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + dispatchPointerUp(target, {pointerType}); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }, + ); it('is called after valid "keydown" and "keyup" events', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(keydown({key: 'Enter'})); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + ref.current.dispatchEvent(keyup({key: 'Enter'})); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); @@ -478,58 +355,33 @@ describe('Event responder: Press', () => { bottom: 100, right: 100, }); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - pointerType: 'pen', - clientX: 0, - clientY: 0, - }), - ); - expect(onPress).toHaveBeenCalledTimes(1); - expect(onPress).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'press'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerUp(target, {pointerType, x: 10, y: 10}); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'press'}), + ); + }, + ); it('is not called after auxillary-button press', () => { - const Component = () => { - const listener = usePressResponder({ - onPress, - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.dispatchEvent(createEvent('pointerdown', {button: 1})); - ref.current.dispatchEvent( - createEvent('pointerup', {button: 1, clientX: 10, clientY: 10}), - ); + const target = ref.current; + dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); + dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); expect(onPress).not.toHaveBeenCalled(); }); it('is called after valid "keyup" event', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'press'}), @@ -545,26 +397,21 @@ describe('Event responder: Press', () => { return ; }; ReactDOM.render(, container); - inputRef.current.dispatchEvent( - createKeyboardEvent('keydown', {key: 'Enter'}), - ); - inputRef.current.dispatchEvent( - createKeyboardEvent('keyup', {key: 'Enter'}), - ); - inputRef.current.dispatchEvent( - createKeyboardEvent('keydown', {key: ' '}), - ); - inputRef.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '})); + const target = inputRef.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'Enter'})); + target.dispatchEvent(keydown({key: ' '})); + target.dispatchEvent(keyup({key: ' '})); expect(onPress).not.toBeCalled(); }); it('is called with modifier keys', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {metaKey: true, pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {metaKey: true, pointerType: 'mouse'}), - ); + const target = ref.current; + dispatchPointerDown(target, {metaKey: true, pointerType: 'mouse'}); + dispatchPointerUp(target, { + metaKey: true, + pointerType: 'mouse', + }); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({ pointerType: 'mouse', @@ -596,28 +443,19 @@ describe('Event responder: Press', () => { bottom: 0, top: 0, }); - buttonRef.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse'}), - ); - buttonRef.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'mouse'}), - ); + const target = buttonRef.current; + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); expect(onPress).toBeCalled(); }); - - // No PointerEvent fallbacks - // TODO: jsdom missing APIs - // it('is called after "touchend" event', () => { - // ref.current.dispatchEvent(createEvent('touchstart')); - // ref.current.dispatchEvent(createEvent('touchend')); - // expect(onPress).toHaveBeenCalledTimes(1); - // }); }); describe('onPressMove', () => { - it('is called after "pointermove"', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); + let onPressMove, ref; + + beforeEach(() => { + onPressMove = jest.fn(); + ref = React.createRef(); const Component = () => { const listener = usePressResponder({ onPressMove, @@ -625,102 +463,90 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - clientX: 10, - clientY: 10, - }), - ); - expect(onPressMove).toHaveBeenCalledTimes(1); - expect(onPressMove).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}), - ); + document.elementFromPoint = () => ref.current; }); - it('is not called if "pointermove" occurs during keyboard press', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); - const Component = () => { - const listener = usePressResponder({ - onPressMove, + it.each(pointerTypesTable)( + 'is called after pointer move: %s', + pointerType => { + const target = ref.current; + target.getBoundingClientRect = () => ({ + top: 0, + left: 0, + bottom: 100, + right: 100, }); - return
; - }; - ReactDOM.render(, container); + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, { + pointerType, + x: 10, + y: 10, + }); + dispatchPointerMove(target, { + pointerType, + x: 20, + y: 20, + }); + expect(onPressMove).toHaveBeenCalledTimes(2); + expect(onPressMove).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressmove'}), + ); + }, + ); - ref.current.getBoundingClientRect = () => ({ + it('is not called if pointer move occurs during keyboard press', () => { + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - clientX: 10, - clientY: 10, - }), - ); + target.dispatchEvent(keydown({key: 'Enter'})); + dispatchPointerMove(target, { + button: -1, + pointerType: 'mouse', + x: 10, + y: 10, + }); expect(onPressMove).not.toBeCalled(); }); + }); - it('ignores browser emulated events', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); + describe.each(pointerTypesTable)('press with movement: %s', pointerType => { + let events, ref, outerRef; + + beforeEach(() => { + events = []; + ref = React.createRef(); + outerRef = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; const Component = () => { const listener = usePressResponder({ - onPressMove, + onPress: createEventHandler('onPress'), + onPressChange: createEventHandler('onPressChange'), + onPressMove: createEventHandler('onPressMove'), + onPressStart: createEventHandler('onPressStart'), + onPressEnd: createEventHandler('onPressEnd'), }); - return
; + return ( +
+
+
+ ); }; ReactDOM.render(, container); - - ref.current.getBoundingClientRect = () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'touch', - clientX: 10, - clientY: 10, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - target: ref.current, - clientX: 10, - clientY: 10, - }), - ); - ref.current.dispatchEvent(createEvent('mousemove')); - expect(onPressMove).toHaveBeenCalledTimes(1); + document.elementFromPoint = () => ref.current; }); - }); - describe('press with movement (pointer events)', () => { const rectMock = { width: 100, height: 100, @@ -732,12 +558,12 @@ describe('Event responder: Press', () => { const pressRectOffset = 20; const getBoundingClientRectMock = () => rectMock; const coordinatesInside = { - clientX: rectMock.left - pressRectOffset, - clientY: rectMock.top - pressRectOffset, + x: rectMock.left - pressRectOffset, + y: rectMock.top - pressRectOffset, }; const coordinatesOutside = { - clientX: rectMock.left - pressRectOffset - 1, - clientY: rectMock.top - pressRectOffset - 1, + x: rectMock.left - pressRectOffset - 1, + y: rectMock.top - pressRectOffset - 1, }; describe('within bounds of hit rect', () => { @@ -749,32 +575,12 @@ describe('Event responder: Press', () => { * └──────────────────┘ */ it('"onPress*" events are called immediately', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); jest.runAllTimers(); - expect(events).toEqual([ 'onPressStart', 'onPressChange', @@ -786,12 +592,41 @@ describe('Event responder: Press', () => { }); it('"onPress*" events are correctly called with target change', () => { - let events = []; - const outerRef = React.createRef(); - const innerRef = React.createRef(); + const target = ref.current; + const outer = outerRef.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + // TODO: this sequence may differ in the future between PointerEvent and mouse fallback when + // use 'setPointerCapture'. + if (pointerType === 'touch') { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(outer, {pointerType, ...coordinatesOutside}); + } + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); + + expect(events.filter(removePressMoveStrings)).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('press retention offset can be configured', () => { + let localEvents = []; + const localRef = React.createRef(); const createEventHandler = msg => () => { - events.push(msg); + localEvents.push(msg); }; + const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; const Component = () => { const listener = usePressResponder({ @@ -800,72 +635,22 @@ describe('Event responder: Press', () => { onPressMove: createEventHandler('onPressMove'), onPressStart: createEventHandler('onPressStart'), onPressEnd: createEventHandler('onPressEnd'), + pressRetentionOffset, }); - return ( -
-
-
- ); + return
; }; ReactDOM.render(, container); - innerRef.current.getBoundingClientRect = getBoundingClientRectMock; - innerRef.current.dispatchEvent(createEvent('pointerdown')); - outerRef.current.dispatchEvent( - createEvent('pointermove', coordinatesOutside), - ); - innerRef.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - innerRef.current.dispatchEvent( - createEvent('pointerup', coordinatesInside), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressEnd', - 'onPressChange', - 'onPressStart', - 'onPressChange', - 'onPressEnd', - 'onPressChange', - 'onPress', - ]); - }); - - it('press retention offset can be configured', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - pressRetentionOffset, - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', { - clientX: rectMock.left - pressRetentionOffset.left, - clientY: rectMock.top - pressRetentionOffset.top, - }), - ); - ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); - expect(events).toEqual([ + const target = localRef.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, { + pointerType, + x: rectMock.left, + y: rectMock.top, + }); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); + expect(localEvents).toEqual([ 'onPressStart', 'onPressChange', 'onPressMove', @@ -876,26 +661,11 @@ describe('Event responder: Press', () => { }); it('responder region accounts for decrease in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); // emulate smaller dimensions change on activation - ref.current.getBoundingClientRect = () => ({ + target.getBoundingClientRect = () => ({ width: 80, height: 80, top: 60, @@ -904,36 +674,28 @@ describe('Event responder: Press', () => { bottom: 490, }); const coordinates = { - clientX: rectMock.left, - clientY: rectMock.top, + x: rectMock.left, + y: rectMock.top, }; // move to an area within the pre-activation region - ref.current.dispatchEvent(createEvent('pointermove', coordinates)); - ref.current.dispatchEvent(createEvent('pointerup', coordinates)); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); + dispatchPointerMove(target, {pointerType, ...coordinates}); + dispatchPointerUp(target, {pointerType, ...coordinates}); + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); }); it('responder region accounts for increase in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); // emulate larger dimensions change on activation - ref.current.getBoundingClientRect = () => ({ + target.getBoundingClientRect = () => ({ width: 200, height: 200, top: 0, @@ -942,397 +704,12 @@ describe('Event responder: Press', () => { bottom: 550, }); const coordinates = { - clientX: rectMock.left - 50, - clientY: rectMock.top - 50, + x: rectMock.left - 50, + y: rectMock.top - 50, }; // move to an area within the post-activation region - ref.current.dispatchEvent(createEvent('pointermove', coordinates)); - ref.current.dispatchEvent(createEvent('pointerup', coordinates)); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); - }); - }); - - describe('beyond bounds of hit rect', () => { - /** ┌──────────────────┐ - * │ ┌────────────┐ │ - * │ │ VisualRect │ │ - * │ └────────────┘ │ - * │ HitRect │ - * └──────────────────┘ - * X <= Move to X and release - */ - - it('"onPress" is not called on release', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - container.dispatchEvent(createEvent('pointermove', coordinatesOutside)); - container.dispatchEvent(createEvent('pointerup', coordinatesOutside)); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - ]); - }); - }); - - it('"onPress" is not called on release with mouse', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointerup', { - ...coordinatesOutside, - pointerType: 'mouse', - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - ]); - }); - - it('"onPress" is called on re-entry to hit rect for mouse', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointerup', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - 'onPressStart', - 'onPressChange', - 'onPressEnd', - 'onPressChange', - 'onPress', - ]); - }); - - it('"onPress" is called on re-entry to hit rect for touch', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: ref.current, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - container.dispatchEvent( - createEvent('pointerup', { - ...coordinatesInside, - pointerType: 'touch', - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - 'onPressStart', - 'onPressChange', - 'onPressEnd', - 'onPressChange', - 'onPress', - ]); - }); - }); - - describe('press with movement (touch events fallback)', () => { - const rectMock = { - width: 100, - height: 100, - top: 50, - left: 50, - right: 150, - bottom: 150, - }; - const pressRectOffset = 20; - const getBoundingClientRectMock = () => rectMock; - const coordinatesInside = { - clientX: rectMock.left - pressRectOffset, - clientY: rectMock.top - pressRectOffset, - }; - const coordinatesOutside = { - clientX: rectMock.left - pressRectOffset - 1, - clientY: rectMock.top - pressRectOffset - 1, - }; - - describe('within bounds of hit rect', () => { - /** ┌──────────────────┐ - * │ ┌────────────┐ │ - * │ │ VisualRect │ │ - * │ └────────────┘ │ - * │ HitRect X │ <= Move to X and release - * └──────────────────┘ - */ - it('"onPress*" events are called immediately', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - 'onPress', - ]); - }); - - it('press retention offset can be configured', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - pressRetentionOffset, - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - clientX: rectMock.left - pressRetentionOffset.left, - clientY: rectMock.top - pressRetentionOffset.top, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); + dispatchPointerMove(target, {pointerType, ...coordinates}); + dispatchPointerUp(target, {pointerType, ...coordinates}); expect(events).toEqual([ 'onPressStart', 'onPressChange', @@ -1342,112 +719,6 @@ describe('Event responder: Press', () => { 'onPress', ]); }); - - it('responder region accounts for decrease in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - // emulate smaller dimensions change on activation - ref.current.getBoundingClientRect = () => ({ - width: 80, - height: 80, - top: 60, - left: 60, - right: 140, - bottom: 140, - }); - const coordinates = { - clientX: rectMock.left, - clientY: rectMock.top, - }; - // move to an area within the pre-activation region - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinates, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinates, - target: ref.current, - }), - ); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); - }); - - it('responder region accounts for increase in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - // emulate larger dimensions change on activation - ref.current.getBoundingClientRect = () => ({ - width: 200, - height: 200, - top: 0, - left: 0, - right: 200, - bottom: 200, - }); - const coordinates = { - clientX: rectMock.left - 50, - clientY: rectMock.top - 50, - }; - // move to an area within the post-activation region - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinates, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinates, - target: ref.current, - }), - ); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); - }); }); describe('beyond bounds of hit rect', () => { @@ -1459,117 +730,40 @@ describe('Event responder: Press', () => { * └──────────────────┘ * X <= Move to X and release */ - it('"onPress" is not called on release', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => container; - container.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: container, - }), - ); - container.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesOutside, - target: container, - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - ]); - }); - }); - - it('"onPress" is called on re-entry to hit rect for touch', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => container; - container.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: container, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => ref.current; - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - jest.runAllTimers(); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + if (pointerType === 'mouse') { + // TODO: use setPointerCapture so this is only true for fallback mouse events. + dispatchPointerMove(container, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } + dispatchPointerUp(container, {pointerType, ...coordinatesOutside}); + expect(events.filter(removePressMoveStrings)).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + ]); + }); + }); + + it('"onPress" is called on re-entry to hit rect', () => { + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + if (pointerType === 'mouse') { + // TODO: use setPointerCapture so this is only true for fallback mouse events. + dispatchPointerMove(container, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); expect(events).toEqual([ 'onPressStart', @@ -1587,71 +781,71 @@ describe('Event responder: Press', () => { }); describe('nested responders', () => { - it('dispatch events in the correct order', () => { - const events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; + if (hasPointerEvents) { + it('dispatch events in the correct order', () => { + const events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; - const Inner = () => { - const listener = usePressResponder({ - onPress: createEventHandler('inner: onPress'), - onPressChange: createEventHandler('inner: onPressChange'), - onPressMove: createEventHandler('inner: onPressMove'), - onPressStart: createEventHandler('inner: onPressStart'), - onPressEnd: createEventHandler('inner: onPressEnd'), - stopPropagation: false, - }); - return ( -
- ); - }; + const Inner = () => { + const listener = usePressResponder({ + onPress: createEventHandler('inner: onPress'), + onPressChange: createEventHandler('inner: onPressChange'), + onPressMove: createEventHandler('inner: onPressMove'), + onPressStart: createEventHandler('inner: onPressStart'), + onPressEnd: createEventHandler('inner: onPressEnd'), + stopPropagation: false, + }); + return ( +
+ ); + }; - const Outer = () => { - const listener = usePressResponder({ - onPress: createEventHandler('outer: onPress'), - onPressChange: createEventHandler('outer: onPressChange'), - onPressMove: createEventHandler('outer: onPressMove'), - onPressStart: createEventHandler('outer: onPressStart'), - onPressEnd: createEventHandler('outer: onPressEnd'), - }); - return ( -
- -
- ); - }; - ReactDOM.render(, container); + const Outer = () => { + const listener = usePressResponder({ + onPress: createEventHandler('outer: onPress'), + onPressChange: createEventHandler('outer: onPressChange'), + onPressMove: createEventHandler('outer: onPressMove'), + onPressStart: createEventHandler('outer: onPressStart'), + onPressEnd: createEventHandler('outer: onPressEnd'), + }); + return ( +
+ +
+ ); + }; + ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, + const target = ref.current; + target.getBoundingClientRect = () => ({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }); + dispatchPointerDown(target); + dispatchPointerUp(target); + expect(events).toEqual([ + 'inner: onPressStart', + 'inner: onPressChange', + 'pointerdown', + 'inner: onPressEnd', + 'inner: onPressChange', + 'inner: onPress', + 'pointerup', + ]); }); - - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', {clientX: 10, clientY: 10}), - ); - expect(events).toEqual([ - 'inner: onPressStart', - 'inner: onPressChange', - 'pointerdown', - 'inner: onPressEnd', - 'inner: onPressChange', - 'inner: onPress', - 'pointerup', - ]); - }); + } describe('correctly not propagate', () => { it('for onPress', () => { @@ -1677,17 +871,15 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', {clientX: 10, clientY: 10}), - ); + dispatchPointerDown(target); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(1); }); @@ -1717,10 +909,11 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(0); - ref.current.dispatchEvent(createEvent('pointerup')); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); }); @@ -1748,16 +941,17 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); expect(fn).toHaveBeenCalledTimes(1); - ref.current.dispatchEvent(createEvent('pointerup')); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(2); }); }); }); describe('link components', () => { - it('prevents native behaviour for pointer events by default', () => { + it('prevents native behavior by default', () => { const onPress = jest.fn(); const preventDefault = jest.fn(); const ref = React.createRef(); @@ -1770,14 +964,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1797,9 +986,10 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); - ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(click({preventDefault})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1823,14 +1013,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - buttonRef.current.dispatchEvent(createEvent('pointerdown')); - buttonRef.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - buttonRef.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = buttonRef.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); }); @@ -1851,14 +1036,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1879,19 +1059,9 @@ describe('Event responder: Press', () => { ReactDOM.render(, container); ['metaKey', 'ctrlKey', 'shiftKey'].forEach(modifierKey => { - ref.current.dispatchEvent( - createEvent('pointerdown', {[modifierKey]: true}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - [modifierKey]: true, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('click', {[modifierKey]: true, preventDefault}), - ); + const target = ref.current; + dispatchPointerDown(target, {[modifierKey]: true}); + dispatchPointerUp(target, {[modifierKey]: true, preventDefault}); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1913,14 +1083,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1941,9 +1106,10 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); - ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(click({preventDefault})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1952,7 +1118,7 @@ describe('Event responder: Press', () => { }); describe('responder cancellation', () => { - it('ends on "pointercancel", "touchcancel", "scroll", and "dragstart"', () => { + it.each(pointerTypesTable)('ends on pointer cancel', pointerType => { const onPressEnd = jest.fn(); const ref = React.createRef(); @@ -1964,64 +1130,14 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - // Should cancel for non-mouse events - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent(createEvent('scroll')); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Should not cancel for mouse events - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent(createEvent('scroll')); - expect(onPressEnd).toHaveBeenCalledTimes(0); - - // When pointer events are supported - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointercancel', { - pointerType: 'mouse', - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Touch fallback - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchcancel', 0, { - target: ref.current, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Mouse fallback - ref.current.dispatchEvent(createEvent('mousedown')); - ref.current.dispatchEvent(createEvent('dragstart')); + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerCancel(target, {pointerType}); expect(onPressEnd).toHaveBeenCalledTimes(1); }); }); - it('does end on "scroll" to document', () => { + it('does end on "scroll" to document (not mouse)', () => { const onPressEnd = jest.fn(); const ref = React.createRef(); @@ -2033,12 +1149,13 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - document.dispatchEvent(createEvent('scroll')); + const target = ref.current; + dispatchPointerDown(target, {pointerType: 'touch'}); + document.dispatchEvent(scroll()); expect(onPressEnd).toHaveBeenCalledTimes(1); }); - it('does end on "scroll" to a parent container', () => { + it('does end on "scroll" to a parent container (not mouse)', () => { const onPressEnd = jest.fn(); const ref = React.createRef(); const containerRef = React.createRef(); @@ -2055,8 +1172,8 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - containerRef.current.dispatchEvent(createEvent('scroll')); + dispatchPointerDown(ref.current, {pointerType: 'touch'}); + containerRef.current.dispatchEvent(scroll()); expect(onPressEnd).toHaveBeenCalledTimes(1); }); @@ -2078,8 +1195,8 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - outsideRef.current.dispatchEvent(createEvent('scroll')); + dispatchPointerDown(ref.current); + outsideRef.current.dispatchEvent(scroll()); expect(onPressEnd).not.toBeCalled(); }); @@ -2095,10 +1212,11 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointermove')); - ref.current.dispatchEvent(createEvent('pointerup')); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerMove(target); + dispatchPointerUp(target); + dispatchPointerDown(target); }); it('should correctly pass through event properties', () => { @@ -2133,57 +1251,49 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 10, left: 10, bottom: 110, right: 110, }); - - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - pageX: 15, - pageY: 16, - screenX: 20, - screenY: 21, - clientX: 30, - clientY: 31, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - pageX: 16, - pageY: 17, - screenX: 21, - screenY: 22, - clientX: 31, - clientY: 32, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - pointerType: 'mouse', - pageX: 17, - pageY: 18, - screenX: 22, - screenY: 23, - clientX: 32, - clientY: 33, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - pageX: 18, - pageY: 19, - screenX: 23, - screenY: 24, - clientX: 33, - clientY: 34, - }), - ); + dispatchPointerDown(target, { + pointerType: 'mouse', + pageX: 15, + pageY: 16, + screenX: 20, + screenY: 21, + clientX: 30, + clientY: 31, + }); + dispatchPointerMove(target, { + pointerType: 'mouse', + pageX: 16, + pageY: 17, + screenX: 21, + screenY: 22, + clientX: 31, + clientY: 32, + }); + dispatchPointerUp(target, { + pointerType: 'mouse', + pageX: 17, + pageY: 18, + screenX: 22, + screenY: 23, + clientX: 32, + clientY: 33, + }); + dispatchPointerDown(target, { + pointerType: 'mouse', + pageX: 18, + pageY: 19, + screenX: 23, + screenY: 24, + clientX: 33, + clientY: 34, + }); expect(typeof timeStamps[0] === 'number').toBe(true); expect(eventLog).toEqual([ { @@ -2249,175 +1359,85 @@ describe('Event responder: Press', () => { ]); }); - function dispatchEventWithTimeStamp(elem, name, timeStamp) { - const event = createEvent(name, { - clientX: 0, - clientY: 0, - }); - Object.defineProperty(event, 'timeStamp', { - value: timeStamp, - }); - elem.dispatchEvent(event); - } - - it('should properly only flush sync once when the event systems are mixed', () => { - const ref = React.createRef(); - let renderCounts = 0; - - function MyComponent() { - const [, updateCounter] = React.useState(0); - renderCounts++; - - function handlePress() { - updateCounter(count => count + 1); - } - - const listener = usePressResponder({ - onPress: handlePress, - }); - - return ( -
- -
- ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - dispatchEventWithTimeStamp(ref.current, 'click', 100); - - if (__DEV__) { - expect(renderCounts).toBe(2); - } else { - expect(renderCounts).toBe(1); - } - Scheduler.unstable_flushAll(); - if (__DEV__) { - expect(renderCounts).toBe(4); - } else { - expect(renderCounts).toBe(2); - } - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - // Ensure the timeStamp logic works - dispatchEventWithTimeStamp(ref.current, 'click', 101); - - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } + if (hasPointerEvents) { + it('should properly only flush sync once when the event systems are mixed', () => { + const ref = React.createRef(); + let renderCounts = 0; - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); + function MyComponent() { + const [, updateCounter] = React.useState(0); + renderCounts++; - it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { - const ref = React.createRef(); - let renderCounts = 0; + function handlePress() { + updateCounter(count => count + 1); + } - function MyComponent() { - const [, updateCounter] = React.useState(0); - renderCounts++; + const listener = usePressResponder({ + onPress: handlePress, + }); - function handlePress() { - updateCounter(count => count + 1); + return ( +
+ +
+ ); } - const listener = usePressResponder({ - onPress: handlePress, - }); - - return ( -
- -
- ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - dispatchEventWithTimeStamp(ref.current, 'click', 100); - - if (__DEV__) { - expect(renderCounts).toBe(4); - } else { - expect(renderCounts).toBe(2); - } - Scheduler.unstable_flushAll(); - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } + const newContainer = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(newContainer); + document.body.appendChild(newContainer); + root.render(); + Scheduler.unstable_flushAll(); - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - // Ensure the timeStamp logic works - dispatchEventWithTimeStamp(ref.current, 'click', 101); + const target = ref.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); - if (__DEV__) { - expect(renderCounts).toBe(8); - } else { - expect(renderCounts).toBe(4); - } + if (__DEV__) { + expect(renderCounts).toBe(2); + } else { + expect(renderCounts).toBe(1); + } + Scheduler.unstable_flushAll(); + if (__DEV__) { + expect(renderCounts).toBe(4); + } else { + expect(renderCounts).toBe(2); + } - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + // Ensure the timeStamp logic works + target.dispatchEvent(click({timeStamp: 101})); - it( - 'should only flush before outermost discrete event handler when mixing ' + - 'event systems', - async () => { - const {useState} = React; + if (__DEV__) { + expect(renderCounts).toBe(6); + } else { + expect(renderCounts).toBe(3); + } - const button = React.createRef(); + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); - const ops = []; + it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { + const ref = React.createRef(); + let renderCounts = 0; function MyComponent() { - const [pressesCount, updatePressesCount] = useState(0); - const [clicksCount, updateClicksCount] = useState(0); + const [, updateCounter] = React.useState(0); + renderCounts++; function handlePress() { - // This dispatches a synchronous, discrete event in the legacy event - // system. However, because it's nested inside the new event system, - // its updates should not flush until the end of the outer handler. - button.current.click(); - // Text context should not have changed - ops.push(newContainer.textContent); - updatePressesCount(pressesCount + 1); + updateCounter(count => count + 1); } const listener = usePressResponder({ @@ -2427,76 +1447,131 @@ describe('Event responder: Press', () => { return (
); } const newContainer = document.createElement('div'); - document.body.appendChild(newContainer); const root = ReactDOM.unstable_createRoot(newContainer); - + document.body.appendChild(newContainer); root.render(); Scheduler.unstable_flushAll(); - expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0'); - dispatchEventWithTimeStamp(button.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(button.current, 'pointerup', 100); - dispatchEventWithTimeStamp(button.current, 'click', 100); + const target = ref.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); + + if (__DEV__) { + expect(renderCounts).toBe(4); + } else { + expect(renderCounts).toBe(2); + } Scheduler.unstable_flushAll(); - expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1'); + if (__DEV__) { + expect(renderCounts).toBe(6); + } else { + expect(renderCounts).toBe(3); + } - expect(ops).toEqual(['Presses: 0, Clicks: 0']); - }, - ); + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + // Ensure the timeStamp logic works + target.dispatchEvent(click({timeStamp: 101})); - it('should work correctly with stopPropagation set to true', () => { - const ref = React.createRef(); - const pointerDownEvent = jest.fn(); + if (__DEV__) { + expect(renderCounts).toBe(8); + } else { + expect(renderCounts).toBe(4); + } - const Component = () => { - const listener = usePressResponder({stopPropagation: true}); + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); - return
; - }; + it( + 'should only flush before outermost discrete event handler when mixing ' + + 'event systems', + async () => { + const {useState} = React; - container.addEventListener('pointerdown', pointerDownEvent); - ReactDOM.render(, container); + const button = React.createRef(); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 0}), - ); - container.removeEventListener('pointerdown', pointerDownEvent); - expect(pointerDownEvent).toHaveBeenCalledTimes(0); - }); + const ops = []; - it('has the correct press target when used with event hook', () => { - const ref = React.createRef(); - const onPress = jest.fn(); - const Component = () => { - const listener = usePressResponder({onPress}); + function MyComponent() { + const [pressesCount, updatePressesCount] = useState(0); + const [clicksCount, updateClicksCount] = useState(0); - return ( -
- -
- ); - }; - ReactDOM.render(, container); + function handlePress() { + // This dispatches a synchronous, discrete event in the legacy event + // system. However, because it's nested inside the new event system, + // its updates should not flush until the end of the outer handler. + button.current.click(); + // Text context should not have changed + ops.push(newContainer.textContent); + updatePressesCount(pressesCount + 1); + } - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 0}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'mouse', button: 0}), - ); - expect(onPress).toHaveBeenCalledTimes(1); - expect(onPress).toHaveBeenCalledWith( - expect.objectContaining({target: ref.current}), + const listener = usePressResponder({ + onPress: handlePress, + }); + + return ( +
+ +
+ ); + } + + const newContainer = document.createElement('div'); + document.body.appendChild(newContainer); + const root = ReactDOM.unstable_createRoot(newContainer); + + root.render(); + Scheduler.unstable_flushAll(); + expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0'); + + const target = button.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); + + Scheduler.unstable_flushAll(); + expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1'); + + expect(ops).toEqual(['Presses: 0, Clicks: 0']); + }, ); - }); + + it('should work correctly with stopPropagation set to true', () => { + const ref = React.createRef(); + const pointerDownEvent = jest.fn(); + + const Component = () => { + const listener = usePressResponder({stopPropagation: true}); + return
; + }; + + container.addEventListener('pointerdown', pointerDownEvent); + ReactDOM.render(, container); + dispatchPointerDown(ref.current); + container.removeEventListener('pointerdown', pointerDownEvent); + expect(pointerDownEvent).toHaveBeenCalledTimes(0); + }); + } }); diff --git a/packages/react-events/src/dom/test-utils.js b/packages/react-events/src/dom/test-utils.js index 826ac219ce58f..9597af0781946 100644 --- a/packages/react-events/src/dom/test-utils.js +++ b/packages/react-events/src/dom/test-utils.js @@ -15,7 +15,7 @@ * Change environment support for PointerEvent. */ -function hasPointerEvent(bool) { +function hasPointerEvent() { return global != null && global.PointerEvent != null; } @@ -57,23 +57,38 @@ const platform = { * Mock native events */ -function createEvent(type, data) { +function createEvent(type, data = {}) { const event = document.createEvent('CustomEvent'); event.initCustomEvent(type, true, true); + event.clientX = data.x || 0; + event.clientY = data.y || 0; + event.x = data.x || 0; + event.y = data.y || 0; if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; + Object.keys(data).forEach(key => { + const value = data[key]; + Object.defineProperty(event, key, {value}); }); } return event; } -function createTouchEvent(type, data, id) { +function createTouchEvent(type, data = {}, id) { return createEvent(type, { changedTouches: [ { + identifier: id, + clientX: data.x || 0, + clientY: data.y || 0, ...data, + }, + ], + targetTouches: [ + { identifier: id, + clientX: 0 || data.x, + clientY: 0 || data.y, + ...data, }, ], }); @@ -112,17 +127,45 @@ function gotpointercapture(data) { } function keydown(data) { - return createKeyboardEvent('keydown', data); + return createEvent('keydown', data); } function keyup(data) { - return createKeyboardEvent('keyup', data); + return createEvent('keyup', data); } function lostpointercapture(data) { return createEvent('lostpointercapture', data); } +function mousedown(data) { + return createEvent('mousedown', data); +} + +function mouseenter(data) { + return createEvent('mouseenter', data); +} + +function mouseleave(data) { + return createEvent('mouseleave', data); +} + +function mousemove(data) { + return createEvent('mousemove', data); +} + +function mouseout(data) { + return createEvent('mouseout', data); +} + +function mouseover(data) { + return createEvent('mouseover', data); +} + +function mouseup(data) { + return createEvent('mouseup', data); +} + function pointercancel(data) { return createEvent('pointercancel', data); } @@ -155,32 +198,8 @@ function pointerup(data) { return createEvent('pointerup', data); } -function mousedown(data) { - return createEvent('mousedown', data); -} - -function mouseenter(data) { - return createEvent('mouseenter', data); -} - -function mouseleave(data) { - return createEvent('mouseleave', data); -} - -function mousemove(data) { - return createEvent('mousemove', data); -} - -function mouseout(data) { - return createEvent('mouseout', data); -} - -function mouseover(data) { - return createEvent('mouseover', data); -} - -function mouseup(data) { - return createEvent('mouseup', data); +function scroll(data) { + return createEvent('scroll', data); } function touchcancel(data, id) { @@ -264,15 +283,15 @@ function dispatchPointerHoverEnter(target, {relatedTarget, x, y} = {}) { dispatch(pointerenter({pointerType, ...event})); } dispatch(mouseover(event)); - dispatch(mouseover(event)); + dispatch(mouseenter(event)); } -function dispatchPointerHoverMove(target, {from, to} = {}) { +function dispatchPointerHoverMove(target, {x, y} = {}) { const dispatch = arg => target.dispatchEvent(arg); const button = -1; const pointerId = 1; const pointerType = 'mouse'; - function dispatchMove({x, y}) { + function dispatchMove() { const event = { button, clientX: x, @@ -285,8 +304,7 @@ function dispatchPointerHoverMove(target, {from, to} = {}) { } dispatch(mousemove(event)); } - dispatchMove({x: from.x, y: from.y}); - dispatchMove({x: to.x, y: to.y}); + dispatchMove(); } function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) { @@ -309,77 +327,135 @@ function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) { dispatch(mouseleave(event)); } -function dispatchPointerCancel(target, options) { +function dispatchPointerCancel(target, {pointerType = 'mouse', ...rest} = {}) { const dispatchEvent = arg => target.dispatchEvent(arg); - dispatchEvent(pointercancel({pointerType: 'mouse'})); - dispatchEvent(dragstart({pointerType: 'mouse'})); + if (hasPointerEvent()) { + dispatchEvent(pointercancel({pointerType, ...rest})); + } else { + if (pointerType === 'mouse') { + dispatchEvent(dragstart({...rest})); + } else { + dispatchEvent(touchcancel({...rest})); + } + } } -function dispatchPointerPressDown( +function dispatchPointerDown( target, - {button = 0, pointerType = 'mouse'} = {}, + {button = 0, pointerType = 'mouse', ...rest} = {}, ) { const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (pointerType !== 'mouse') { + const pointerEvent = {button, pointerId, pointerType, ...rest}; + const mouseEvent = {button, ...rest}; + const touch = {...rest}; + + if (pointerType === 'mouse') { if (hasPointerEvent()) { - dispatch(pointerover({button, pointerId, pointerType})); - dispatch(pointerenter({button, pointerId, pointerType})); - dispatch(pointerdown({button, pointerId, pointerType})); + dispatch(pointerover(pointerEvent)); + dispatch(pointerenter(pointerEvent)); } - dispatch(touchstart(null, pointerId)); + dispatch(mouseover(mouseEvent)); + dispatch(mouseenter(mouseEvent)); if (hasPointerEvent()) { - dispatch(gotpointercapture({button, pointerId, pointerType})); + dispatch(pointerdown(pointerEvent)); + } + dispatch(mousedown(mouseEvent)); + if (document.activeElement !== target) { + dispatch(focus()); } } else { if (hasPointerEvent()) { - dispatch(pointerdown({button, pointerId, pointerType})); + dispatch(pointerover(pointerEvent)); + dispatch(pointerenter(pointerEvent)); + dispatch(pointerdown(pointerEvent)); } - dispatch(mousedown({button})); - if (document.activeElement !== target) { - dispatch(focus({button})); + dispatch(touchstart(touch, pointerId)); + if (hasPointerEvent()) { + dispatch(gotpointercapture(pointerEvent)); } } } -function dispatchPointerPressRelease( +function dispatchPointerUp( target, - {button = 0, pointerType = 'mouse'} = {}, + {button = 0, pointerType = 'mouse', ...rest} = {}, ) { const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (pointerType !== 'mouse') { + const pointerEvent = {button, pointerId, pointerType, ...rest}; + const mouseEvent = {button, ...rest}; + const touch = {...rest}; + + if (pointerType === 'mouse') { if (hasPointerEvent()) { - dispatch(pointerup({button, pointerId, pointerType})); - dispatch(lostpointercapture({button, pointerId, pointerType})); - dispatch(pointerout({button, pointerId, pointerType})); - dispatch(pointerleave({button, pointerId, pointerType})); - } - dispatch(touchend(null, pointerId)); - dispatch(mouseover({button})); - dispatch(mousemove({button})); - dispatch(mousedown({button})); - if (document.activeElement !== target) { - dispatch(focus({button})); + dispatch(pointerup(pointerEvent)); } - dispatch(mouseup({button})); - dispatch(click({button})); + dispatch(mouseup(mouseEvent)); + dispatch(click(mouseEvent)); } else { if (hasPointerEvent()) { - dispatch(pointerup({button, pointerId, pointerType})); + dispatch(pointerup(pointerEvent)); + dispatch(lostpointercapture(pointerEvent)); + dispatch(pointerout(pointerEvent)); + dispatch(pointerleave(pointerEvent)); } - dispatch(mouseup({button})); - dispatch(click({button})); + dispatch(touchend(touch, pointerId)); + dispatch(mouseover(mouseEvent)); + dispatch(mousemove(mouseEvent)); + dispatch(mousedown(mouseEvent)); + if (document.activeElement !== target) { + dispatch(focus()); + } + dispatch(mouseup(mouseEvent)); + dispatch(click(mouseEvent)); + } +} + +function dispatchPointerMove( + target, + {button = 0, pointerType = 'mouse', ...rest} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + const pointerId = 1; + const pointerEvent = { + button, + pointerId, + pointerType, + ...rest, + }; + const mouseEvent = { + button, + ...rest, + }; + const touch = { + ...rest, + }; + + if (hasPointerEvent()) { + dispatch(pointermove(pointerEvent)); + } + if (pointerType === 'mouse') { + dispatch(mousemove(mouseEvent)); + } + if (pointerType === 'touch') { + dispatch(touchmove(touch, pointerId)); } } function dispatchTouchTap(target) { - dispatchPointerPressDown(target, {pointerType: 'touch'}); - dispatchPointerPressRelease(target, {pointerType: 'touch'}); + dispatchPointerDown(target, {pointerType: 'touch'}); + dispatchPointerUp(target, {pointerType: 'touch'}); +} + +function dispatchMouseTap(target) { + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); } module.exports = { blur, + click, focus, createEvent, dispatchLongPressContextMenu, @@ -389,11 +465,16 @@ module.exports = { dispatchPointerHoverEnter, dispatchPointerHoverExit, dispatchPointerHoverMove, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerMove, + dispatchPointerDown, + dispatchPointerUp, dispatchTouchTap, + dispatchMouseTap, keydown, keyup, + scroll, + pointerdown, + pointerup, platform, hasPointerEvent, setPointerEvent, From 9b5985b3c11929f8fc3bf5092d6c94838c7fe3dc Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Aug 2019 10:40:59 -0700 Subject: [PATCH 02/18] Added release date to DevTools CHANGELOG --- packages/react-devtools/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 47c5d992c1262..6ae043731f07f 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -10,7 +10,7 @@ -## 4.0.0 (release date TBD) +## 4.0.0 (August 15, 2019) ### General changes From 600c57a9b9f36c8de1d3544bdbadd3babba1bcc7 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Aug 2019 11:22:11 -0700 Subject: [PATCH 03/18] Added OVERVIEW.md and updated CHANGELOG to point to it (#16405) --- packages/react-devtools/CHANGELOG.md | 4 +- packages/react-devtools/OVERVIEW.md | 270 +++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 packages/react-devtools/OVERVIEW.md diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 6ae043731f07f..47c21f16b1a66 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -17,7 +17,7 @@ #### Improved performance The legacy DevTools extension used to add significant performance overhead, making it unusable for some larger React applications. That overhead has been effectively eliminated in version 4. -[Learn more](https://github.com/bvaughn/react-devtools-experimental/blob/master/OVERVIEW.md) about the performance optimizations that made this possible. +[Learn more](https://github.com/facebook/react/blob/master/packages/react-devtools/OVERVIEW.md) about the performance optimizations that made this possible. #### Component stacks @@ -43,7 +43,7 @@ Filter preferences are remembered between sessions. #### No more in-line props -Components in the tree no longer show in-line props. This was done to [make DevTools faster](https://github.com/bvaughn/react-devtools-experimental/blob/master/OVERVIEW.md) and to make it easier to browse larger component trees. +Components in the tree no longer show in-line props. This was done to [make DevTools faster](https://github.com/facebook/react/blob/master/packages/react-devtools/OVERVIEW.md) and to make it easier to browse larger component trees. You can view a component's props, state, and hooks by selecting it: diff --git a/packages/react-devtools/OVERVIEW.md b/packages/react-devtools/OVERVIEW.md new file mode 100644 index 0000000000000..7c8228791393a --- /dev/null +++ b/packages/react-devtools/OVERVIEW.md @@ -0,0 +1,270 @@ +# Overview + +The React DevTools extension consists of multiple pieces: +* The **frontend** portion is the extension you see (the Components tree, the Profiler, etc.). +* The **backend** portion is invisible. It runs in the same context as React itself. When React commits changes to e.g. the DOM, the backend is responsible for notifying the frontend by sending a message through the **bridge** (an abstraction around e.g. `postMessage`). + +One of the largest performance bottlenecks of the old React DevTools was the amount of bridge traffic. Each time React commits an update, the backend sends every fiber that changed across the bridge, resulting in a lot of (JSON) serialization. The primary goal for the DevTools rewrite was to reduce this traffic. Instead of sending everything across the bridge, **the backend should only send the minimum amount required to render the Components tree**. The frontend can request more information (e.g. an element's props) on demand, only as needed. + +The old DevTools also rendered the entire application tree in the form of a large DOM structure of nested nodes. A secondary goal of the rewrite was to avoid rendering unnecessary nodes by using a windowing library (specifically [react-window](https://github.com/bvaughn/react-window)). + +## Components panel + +### Serializing the tree + +Every React commit that changes the tree in a way DevTools cares about results in an "_operations_" message being sent across the bridge. These messages are lightweight patches that describe the changes that were made. (We don't resend the full tree structure like in legacy DevTools.) + +The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). Then the strings are encoded in a [string table](#string-table). The rest of the array depends on the operations being made to the tree. + +No updates are required for most commits because we only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate ["_inspectElement_" message](#inspecting-an-element). + +#### String table + +The string table is encoded right after the first two numbers. + +It consists of: + +1. the total length of next items that belong to string table +2. for each string in a table: + 1. encoded size + 2. a list of its UTF encoded codepoints + +For example, for `Foo` and `Bar` we would see: + +``` +[ + 8, // string table length + 3, // encoded display name size + 70, // "F" + 111, // "o" + 111, // "o" + 3, // encoded display name size + 66, // "B" + 97, // "a" + 114, // "r" +] +``` + +Later operations will reference strings by a one-based index. For example, `1` would mean `"Foo"`, and `2` would mean `"Bar"`. The `0` string id always represents `null` and isn't explicitly encoded in the table. + +#### Adding a root node + +Adding a root to the tree requires sending 4 numbers: + +1. add operation constant (`1`) +1. fiber id +1. element type constant (`8 === ElementTypeRoot`) +1. profiling supported flag + +For example, adding a root fiber with an id of 1: +```js +[ + 1, // add operation + 1, // fiber id + 8, // ElementTypeRoot + 1, // this root's renderer supports profiling +] +``` + +#### Adding a leaf node + +Adding a leaf node takes a variable number of numbers since we need to decode the name (and potentially the key): + +1. add operation constant (`1`) +1. fiber id +1. element type constant (e.g. `1 === ElementTypeClass`) +1. parent fiber id +1. owner fiber id +1. string table id for `displayName` +1. string table id for `key` + +For example, adding a function component `` with an id 2: +```js +[ + 1, // add operation + 2, // fiber id + 1, // ElementTypeClass + 1, // parent id + 0, // owner id + 3, // encoded display name size + 1, // id of "Foo" displayName in the string table + 0, // id of null key in the string table (always zero for null) +] +``` + +#### Removing a node + +Removing a fiber from the tree (a root or a leaf) requires sending: + +1. remove operation constant (`2`) +1. how many items were removed +1. number of children + * (followed by a children-first list of removed fiber ids) + +For example, removing fibers with ids of 35 and 21: +```js +[ + 2, // remove operation + 2, // number of removed fibers + 35, // first removed id + 21, // second removed id +] +``` + +#### Re-ordering children + +1. re-order children constant (`3`) +1. fiber id +1. number of children + * (followed by an ordered list of child fiber ids) + +For example: +```js +[ + 3, // re-order operation + 15, // fiber id + 2, // number of children + 35, // first child id + 21, // second child id +] +``` + +#### Updating tree base duration + +While profiling is in progress, we send an extra operation any time a fiber is added or a updated in a way that affects its tree base duration. This information is needed by the Profiler UI in order to render the "snapshot" and "ranked" chart views. + +1. tree base duration constant (`4`) +1. fiber id +1. tree base duration + +For example, updating the base duration for a fiber with an id of 1: +```js +[ + 4, // tree base duration operation + 1, // fiber id + 32, // new tree base duration value +] +``` + +## Reconstructing the tree + +The frontend stores its information about the tree in a map of id to objects with the following keys: + +* id: `number` +* parentID: `number` +* children: `Array` +* type: `number` (constant) +* displayName: `string | null` +* key: `number | string | null` +* ownerID: `number` +* depth: `number` 1 +* weight: `number` 2 + +1 The `depth` value determines how much padding/indentation to use for the element when rendering it in the Components panel. (This preserves the appearance of a nested tree, even though the view is a flat list.) + +2 The `weight` of an element is the number of elements (including itself) below it in the tree. We cache this property so that we can quickly determine the total number of Components as well as to find the Nth element within that set. (This enables us to use windowing.) This value needs to be adjusted each time elements are added or removed from the tree, but we amortize this over time to avoid any big performance hits when rendering the tree. + +#### Finding the element at index N + +The tree data structure lets us impose an order on elements and "quickly" find the Nth one using the `weight` attribute. + +First we find which root contains the index: +```js +let rootID; +let root; +let rootWeight = 0; +for (let i = 0; i < this._roots.length; i++) { + rootID = this._roots[i]; + root = this._idToElement.get(rootID); + if (root.children.length === 0) { + continue; + } else if (rootWeight + root.weight > index) { + break; + } else { + rootWeight += root.weight; + } +} +``` + +We skip the root itself because don't display them in the tree: +```js +const firstChildID = root.children[0]; +``` + +Then we traverse the tree to find the element: +```js +let currentElement = this._idToElement.get(firstChildID); +let currentWeight = rootWeight; +while (index !== currentWeight) { + for (let i = 0; i < currentElement.children.length; i++) { + const childID = currentElement.children[i]; + const child = this._idToElement.get(childID); + const { weight } = child; + if (index <= currentWeight + weight) { + currentWeight++; + currentElement = child; + break; + } else { + currentWeight += weight; + } + } +} +``` + +## Inspecting an element + +When an element is mounted in the tree, DevTools sends a minimal amount of information about it across the bridge. This information includes its display name, type, and key- but does _not_ include things like props or state. (These values are often expensive to serialize and change frequently, which would add a significant amount of load to the bridge.) + +Instead DevTools lazily requests additional information about an element only when it is selected in the "Components" tab. At that point, the frontend requests this information by sending a special "_inspectElement_" message containing the id of the element being inspected. The backend then responds with an "_inspectedElement_" message containing the additional details. + +### Polling strategy + +Elements can update frequently, especially in response to things like scrolling events. Since props and state can be large, we avoid sending this information across the bridge every time the selected element is updated. Instead, the frontend polls the backend for updates about once a second. The backend tracks when the element was last "inspected" and sends a special no-op response if it has not re-rendered since then. + +### Deeply nested properties + +Even when dealing with a single component, serializing deeply nested properties can be expensive. Because of this, DevTools uses a technique referred to as "dehyration" to only send a shallow copy of the data on initial inspection. DevTools then fills in the missing data on demand as a user expands nested objects or arrays. Filled in paths are remembered (for the currently inspected element) so they are not "dehyrated" again as part of a polling update. + +### Inspecting hooks + +Hooks present a unique challenge for the DevTools because of the concept of _custom_ hooks. (A custom hook is essentially any function that calls at least one of the built-in hooks. By convention custom hooks also have names that begin with "use".) + +So how does DevTools identify custom functions called from within third party components? It does this by temporarily overriding React's built-in hooks and shallow rendering the component in question. Whenever one of the (overridden) built-in hooks are called, it parses the call stack to spot potential custom hooks (functions between the component itself and the built-in hook). This approach enables it to build a tree structure describing all of the calls to both the built-in _and_ custom hooks, along with the values passed to those hooks. (If you're interested in learning more about this, [here is the source code](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/ReactDebugHooks.js).) + +> **Note**: DevTools obtains hooks info by re-rendering a component. +> Breakpoints will be invoked during this additional (shallow) render, +> but DevTools temporarily overrides `console` methods to suppress logging. + +### Performance implications + +To mitigate the performance impact of re-rendering a component, DevTools does the following: +* Only function components that use _at least one hook_ are rendered. (Props and state can be analyzed without rendering.) +* Rendering is always shallow. +* Rendering is throttled to occur, at most, once per second. +* Rendering is skipped if the component has not updated since the last time its properties were inspected. + +## Profiler + +The Profiler UI is a powerful tool for identifying and fixing performance problems. The primary goal of the new profiler is to minimize its impact (CPU usage) while profiling is active. This can be accomplished by: +* Minimizing bridge traffic. +* Making expensive computations lazy. + +The majority of profiling information is stored on the backend. The backend push-notifies the frontend of when profiling starts or stops by sending a "_profilingStatus_" message. The frontend also asks for the current status after mounting by sending a "_getProfilingStatus_" message. (This is done to support the reload-and-profile functionality.) + +When profiling begins, the frontend takes a snapshot/copy of each root. This snapshot includes the id, name, key, and child IDs for each node in the tree. (This information is already present on the frontend, so it does not require any additional bridge traffic.) While profiling is active, each time React commits– the frontend also stores a copy of the "_operations_" message (described above). Once profiling has finished, the frontend can use the original snapshot along with each of the stored "_operations_" messages to reconstruct the tree for each of the profiled commits. + +When profiling begins, the backend records the base durations of each fiber currently in the tree. While profiling is in progress, the backend also stores some information about each commit, including: +* Commit time and duration +* Which elements were rendered during that commit +* Which interactions (if any) were part of the commit +* Which props and state changed (if enabled in profiler settings) + +This information will eventually be required by the frontend in order to render its profiling graphs, but it will not be sent across the bridge until profiling has completed (to minimize the performance impact of profiling). + +### Combining profiling data + +Once profiling is finished, the frontend requests profiling data from the backend one renderer at a time by sending a "_getProfilingData_" message. The backend responds with a "_profilingData_" message that contains per-root commit timing and duration information. The frontend then combines this information with its own snapshots to form a complete picture of the profiling session. Using this data, charts and graphs are lazily computed (and incrementally cached) on demand, based on which commits and views are selected in the Profiler UI. + +### Importing/exporting data + +Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users. \ No newline at end of file From 6f86294e686e172fb8da32880f70fa038d644c74 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 15 Aug 2019 23:11:37 +0100 Subject: [PATCH 04/18] [DevTools Changelog] Add a note about restoring selection (#16409) Also a tiny nit, "inline" spelling seems more common in this context. My eyes stumbled at it on every read. --- packages/react-devtools/CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 47c21f16b1a66..c27677d107439 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -41,9 +41,9 @@ Host nodes (e.g. HTML `
`, React Native `View`) are now hidden by default, b Filter preferences are remembered between sessions. -#### No more in-line props +#### No more inline props -Components in the tree no longer show in-line props. This was done to [make DevTools faster](https://github.com/facebook/react/blob/master/packages/react-devtools/OVERVIEW.md) and to make it easier to browse larger component trees. +Components in the tree no longer show inline props. This was done to [make DevTools faster](https://github.com/facebook/react/blob/master/packages/react-devtools/OVERVIEW.md) and to make it easier to browse larger component trees. You can view a component's props, state, and hooks by selecting it: @@ -101,6 +101,12 @@ Components decorated with multiple HOCs show the topmost badge and a count. Sele ![Screenshot showing a component with multiple HOC badges](https://user-images.githubusercontent.com/29597/62303729-7fadbb00-b431-11e9-8685-45f5ab52b30b.png) +#### Restoring selection between reloads + +DevTools now attempts to restore the previously selected element when you reload the page. + +![Video demonstrating selection persistence](https://user-images.githubusercontent.com/810438/63130054-2c02ac00-bfb1-11e9-92fa-382e9e433638.gif) + #### Suspense toggle React's experimental [Suspense API](https://reactjs.org/docs/react-api.html#suspense) lets components "wait" for something before rendering. `` components can be used to specify loading states when components deeper in the tree are waiting to render. @@ -139,4 +145,4 @@ Because this feature adds a small amount of overhead, it can be disabled in the The profiler now displays a list of each time the selected component rendered during a profiling session, along with the duration of each render. This list can be used to quickly jump between commits when analyzing the performance of a specific component. -![Video demonstrating profiler's component renders list](https://user-images.githubusercontent.com/29597/62234547-bcb97500-b37f-11e9-9615-54fba8b574b9.gif) \ No newline at end of file +![Video demonstrating profiler's component renders list](https://user-images.githubusercontent.com/29597/62234547-bcb97500-b37f-11e9-9615-54fba8b574b9.gif) From c1d3f7f1a97adad9441287a92dcd4ac5d2478c38 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 16 Aug 2019 16:15:19 +0100 Subject: [PATCH 05/18] [DevTools Changelog] Add a note on 4.0.2 --- packages/react-devtools/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index c27677d107439..10ba1dde4327a 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -10,6 +10,10 @@ +## 4.0.2 (August 15, 2019) + +* Fixed unnecessary and erroneously requested permissions in the 4.0.0 and 4.0.1 releases. + ## 4.0.0 (August 15, 2019) ### General changes From 21e793fb4fce16b938e549de9277a7fdddff40e8 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 17 Aug 2019 11:47:38 -0700 Subject: [PATCH 06/18] Added 4.0.1, 4.0.2, and 4.0.3 changelog entries (#16438) * Added 4.0.1, 4.0.2, and 4.0.3 changelog entries * Added entry about Map/Set/Immutable --- packages/react-devtools/CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 10ba1dde4327a..3e203b90dd317 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -10,9 +10,21 @@ +## 4.0.3 (August 17, 2019) +#### Bug fixes +* ES6 `Map` and `Set`, typed arrays, and other unnserializable types (e.g. Immutable JS) can now be inspected. +* Empty objects and arrays now display an "(empty)" label to the right to be reduce confusion. +* Components that use only the `useContext` hook now properly display hooks values in side panel. +* Style editor now supports single quotes around string values (e.g. both `"red"` and `'red'`). +* Fixed edge case bug that prevented profiling when both React v16 and v15 were present on a page. + ## 4.0.2 (August 15, 2019) +#### Permissions cleanup +* Removed unnecessary `webNavigation ` permission from Chrome and Firefox extensions. -* Fixed unnecessary and erroneously requested permissions in the 4.0.0 and 4.0.1 releases. +## 4.0.1 (August 15, 2019) +#### Permissions cleanup +* Removed unnecessary ``, `background`, and `tabs` permissions from Chrome and Firefox extensions. ## 4.0.0 (August 15, 2019) From d97af798d2a295a9e38abfe4a5d6b897adabd39e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 17 Aug 2019 21:19:00 -0700 Subject: [PATCH 07/18] Updated DevTools CHANLOGE to add an unreleased change --- packages/react-devtools/CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 3e203b90dd317..e5d883d1274ea 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -1,14 +1,16 @@ # React DevTools changelog - + + +#### Bug fixes +* Standalone DevTools properly serves backend script over localhost:8097 + ## 4.0.3 (August 17, 2019) #### Bug fixes From e89c19d16c5da3f6d57b762f1d5ca88c831fccaf Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 18 Aug 2019 08:55:49 -0700 Subject: [PATCH 08/18] Added DevTools 4.0.4 CHANGELOG entry --- packages/react-devtools/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index e5d883d1274ea..5f2de995a7f0a 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -12,6 +12,10 @@ * Standalone DevTools properly serves backend script over localhost:8097 +## 4.0.4 (August 18, 2019) +#### Bug fixes +* Bugfox for potential error if a min-duration commit filter is applied after selecting a fiber in the Profiler UI. + ## 4.0.3 (August 17, 2019) #### Bug fixes * ES6 `Map` and `Set`, typed arrays, and other unnserializable types (e.g. Immutable JS) can now be inspected. From 56d1b0fb5991e954c21609dce80d9849eda115fe Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 19 Aug 2019 09:21:55 -0700 Subject: [PATCH 09/18] [react-events] DOM event testing library (#16433) 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. --- packages/react-events/src/dom/Press.js | 2 + packages/react-events/src/dom/Scroll.js | 26 +- .../__tests__/ContextMenu-test.internal.js | 26 +- .../src/dom/__tests__/Focus-test.internal.js | 125 ++-- .../__tests__/FocusWithin-test.internal.js | 134 ++-- .../src/dom/__tests__/Hover-test.internal.js | 110 +-- .../dom/__tests__/Keyboard-test.internal.js | 17 +- .../src/dom/__tests__/Press-test.internal.js | 669 +++++++----------- .../src/dom/__tests__/Scroll-test.internal.js | 177 ++--- packages/react-events/src/dom/test-utils.js | 481 ------------- .../src/dom/testing-library/domEnvironment.js | 51 ++ .../dom/testing-library/domEventSequences.js | 175 +++++ .../src/dom/testing-library/domEvents.js | 370 ++++++++++ .../src/dom/testing-library/index.js | 104 +++ 14 files changed, 1243 insertions(+), 1224 deletions(-) delete mode 100644 packages/react-events/src/dom/test-utils.js create mode 100644 packages/react-events/src/dom/testing-library/domEnvironment.js create mode 100644 packages/react-events/src/dom/testing-library/domEventSequences.js create mode 100644 packages/react-events/src/dom/testing-library/domEvents.js create mode 100644 packages/react-events/src/dom/testing-library/index.js diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index 40251331f94bc..f526497097f4c 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -520,6 +520,7 @@ const pressResponderImpl = { const isPressed = state.isPressed; handleStopPropagation(props, context, nativeEvent); + switch (type) { // START case 'pointerdown': @@ -632,6 +633,7 @@ const pressResponderImpl = { const previousPointerType = state.pointerType; handleStopPropagation(props, context, nativeEvent); + switch (type) { // MOVE case 'pointermove': diff --git a/packages/react-events/src/dom/Scroll.js b/packages/react-events/src/dom/Scroll.js index 5fa1e34a8e5e5..44b782b977c72 100644 --- a/packages/react-events/src/dom/Scroll.js +++ b/packages/react-events/src/dom/Scroll.js @@ -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'; @@ -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); } } @@ -262,6 +270,8 @@ const scrollResponderImpl = { const {type} = event; switch (type) { + case 'pointercancel': + case 'pointerup': case 'touchcancel': case 'touchend': { if (state.isTouching) { diff --git a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js index bf2fb0f3e3a7a..b25cbf29fcbd1 100644 --- a/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js +++ b/packages/react-events/src/dom/__tests__/ContextMenu-test.internal.js @@ -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; @@ -62,7 +56,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchRightClickContextMenu(ref.current, {preventDefault}); + const target = createEventTarget(ref.current); + target.contextmenu({preventDefault}); expect(preventDefault).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledTimes(1); expect(onContextMenu).toHaveBeenCalledWith( @@ -80,7 +75,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, 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( @@ -100,7 +96,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchRightClickContextMenu(ref.current); + const target = createEventTarget(ref.current); + target.contextmenu(); expect(onContextMenu).toHaveBeenCalledTimes(0); }); @@ -117,7 +114,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchRightClickContextMenu(ref.current, {preventDefault}); + const target = createEventTarget(ref.current); + target.contextmenu({preventDefault}); expect(preventDefault).toHaveBeenCalledTimes(0); expect(onContextMenu).toHaveBeenCalledTimes(1); }); @@ -142,7 +140,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, 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'}), @@ -169,7 +168,8 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => { }; ReactDOM.render(, container); - dispatchModifiedClickContextMenu(ref.current); + const target = createEventTarget(ref.current); + target.contextmenu({}, {modified: true}); expect(onContextMenu).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/react-events/src/dom/__tests__/Focus-test.internal.js b/packages/react-events/src/dom/__tests__/Focus-test.internal.js index 316bc9f19dcde..12700ce55cc3d 100644 --- a/packages/react-events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Focus-test.internal.js @@ -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; @@ -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(); }); @@ -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); }); }); @@ -127,19 +119,21 @@ 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'}), @@ -147,9 +141,10 @@ describe.each(table)('Focus responder', hasPointerEvents => { }); 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'}), @@ -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'}), @@ -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'}), @@ -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({ @@ -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); }); }); @@ -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); }); }); @@ -338,10 +338,13 @@ describe.each(table)('Focus responder', hasPointerEvents => { ReactDOM.render(, 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', diff --git a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js index ff54b6f3838e1..f6f5ffecb752a 100644 --- a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js @@ -9,14 +9,7 @@ 'use strict'; -import { - blur, - focus, - keydown, - setPointerEvent, - dispatchPointerDown, - dispatchPointerUp, -} from '../test-utils'; +import {createEventTarget, setPointerEvent} from '../testing-library'; let React; let ReactFeatureFlags; @@ -73,9 +66,9 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { }); it('prevents custom events being dispatched', () => { - const target = ref.current; - target.dispatchEvent(focus()); - target.dispatchEvent(blur()); + const target = createEventTarget(ref.current); + target.focus(); + target.blur(); expect(onFocusWithinChange).not.toBeCalled(); expect(onFocusWithinVisibleChange).not.toBeCalled(); }); @@ -104,42 +97,46 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { }); it('is called after "blur" and "focus" events on focus target', () => { - const target = ref.current; - target.dispatchEvent(focus()); + const target = createEventTarget(ref.current); + target.focus(); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); - target.dispatchEvent(blur({relatedTarget: container})); + target.blur({relatedTarget: container}); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); it('is called after "blur" and "focus" events on descendants', () => { - const target = innerRef.current; - target.dispatchEvent(focus()); + const target = createEventTarget(innerRef.current); + target.focus(); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); - target.dispatchEvent(blur({relatedTarget: container})); + target.blur({relatedTarget: container}); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); it('is only called once when focus moves within and outside the subtree', () => { - const target = ref.current; - const innerTarget1 = innerRef.current; - const innerTarget2 = innerRef2.current; + const node = ref.current; + const innerNode1 = innerRef.current; + const innerNode2 = innerRef.current; + const target = createEventTarget(node); + const innerTarget1 = createEventTarget(innerNode1); + const innerTarget2 = createEventTarget(innerNode2); + // focus shifts into subtree - innerTarget1.dispatchEvent(focus()); + innerTarget1.focus(); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); expect(onFocusWithinChange).toHaveBeenCalledWith(true); // focus moves around subtree - innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); - innerTarget2.dispatchEvent(focus()); - innerTarget2.dispatchEvent(blur({relatedTarget: target})); - target.dispatchEvent(focus()); - target.dispatchEvent(blur({relatedTarget: innerTarget1})); + innerTarget1.blur({relatedTarget: innerNode2}); + innerTarget2.focus(); + innerTarget2.blur({relatedTarget: node}); + target.focus(); + target.blur({relatedTarget: innerNode1}); expect(onFocusWithinChange).toHaveBeenCalledTimes(1); // focus shifts outside subtree - innerTarget1.dispatchEvent(blur({relatedTarget: container})); + innerTarget1.blur({relatedTarget: container}); expect(onFocusWithinChange).toHaveBeenCalledTimes(2); expect(onFocusWithinChange).toHaveBeenCalledWith(false); }); @@ -168,87 +165,96 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { }); it('is called after "focus" and "blur" on focus target if keyboard was used', () => { - 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(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); - target.dispatchEvent(blur({relatedTarget: container})); + target.blur({relatedTarget: container}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); it('is called after "focus" and "blur" on descendants if keyboard was used', () => { - const innerTarget = innerRef.current; + const innerTarget = createEventTarget(innerRef.current); + const containerTarget = createEventTarget(container); // use keyboard first - container.dispatchEvent(keydown({key: 'Tab'})); - innerTarget.dispatchEvent(focus()); + containerTarget.keydown({key: 'Tab'}); + innerTarget.focus(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); - innerTarget.dispatchEvent(blur({relatedTarget: container})); + innerTarget.blur({relatedTarget: container}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => { - const target = ref.current; - const innerTarget1 = innerRef.current; - const innerTarget2 = innerRef2.current; + const node = ref.current; + const innerNode1 = innerRef.current; + const innerNode2 = innerRef2.current; + + const target = createEventTarget(node); + const innerTarget1 = createEventTarget(innerNode1); + const innerTarget2 = createEventTarget(innerNode2); // use keyboard first - target.dispatchEvent(focus()); - target.dispatchEvent(keydown({key: 'Tab'})); - target.dispatchEvent(blur({relatedTarget: innerTarget1})); - innerTarget1.dispatchEvent(focus()); + target.focus(); + target.keydown({key: 'Tab'}); + target.blur({relatedTarget: innerNode1}); + innerTarget1.focus(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the next target, focus should no longer be visible - dispatchPointerDown(innerTarget2); - innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); - innerTarget2.dispatchEvent(focus()); + innerTarget2.pointerdown(); + innerTarget1.blur({relatedTarget: innerNode2}); + innerTarget2.focus(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // then use keyboard again - innerTarget2.dispatchEvent(keydown({key: 'Tab', shiftKey: true})); - innerTarget2.dispatchEvent(blur({relatedTarget: innerTarget1})); - innerTarget1.dispatchEvent(focus()); + innerTarget2.keydown({key: 'Tab', shiftKey: true}); + innerTarget2.blur({relatedTarget: innerNode1}); + innerTarget1.focus(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - dispatchPointerDown(innerTarget1); + innerTarget1.pointerdown(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again - innerTarget1.dispatchEvent(blur({relatedTarget: container})); + innerTarget1.blur({relatedTarget: container}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); }); it('is not called after "focus" and "blur" events without keyboard', () => { - const innerTarget = innerRef.current; - dispatchPointerDown(innerTarget); - dispatchPointerUp(innerTarget); - innerTarget.dispatchEvent(blur({relatedTarget: container})); + const innerTarget = createEventTarget(innerRef.current); + innerTarget.pointerdown(); + innerTarget.pointerup(); + innerTarget.blur({relatedTarget: container}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0); }); it('is only called once when focus moves within and outside the subtree', () => { - const target = ref.current; - const innerTarget1 = innerRef.current; - const innerTarget2 = innerRef2.current; + const node = ref.current; + const innerNode1 = innerRef.current; + const innerNode2 = innerRef2.current; + const target = createEventTarget(node); + const innerTarget1 = createEventTarget(innerNode1); + const innerTarget2 = createEventTarget(innerNode2); // focus shifts into subtree - innerTarget1.dispatchEvent(focus()); + innerTarget1.focus(); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // focus moves around subtree - innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); - innerTarget2.dispatchEvent(focus()); - innerTarget2.dispatchEvent(blur({relatedTarget: target})); - target.dispatchEvent(focus()); - target.dispatchEvent(blur({relatedTarget: innerTarget1})); + innerTarget1.blur({relatedTarget: innerNode2}); + innerTarget2.focus(); + innerTarget2.blur({relatedTarget: node}); + target.focus(); + target.blur({relatedTarget: innerNode1}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); // focus shifts outside subtree - innerTarget1.dispatchEvent(blur({relatedTarget: container})); + innerTarget1.blur({relatedTarget: container}); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); }); diff --git a/packages/react-events/src/dom/__tests__/Hover-test.internal.js b/packages/react-events/src/dom/__tests__/Hover-test.internal.js index 2198e74551156..019a2d2b06789 100644 --- a/packages/react-events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Hover-test.internal.js @@ -9,14 +9,7 @@ 'use strict'; -import { - dispatchPointerCancel, - dispatchPointerHoverEnter, - dispatchPointerHoverExit, - dispatchPointerHoverMove, - dispatchTouchTap, - setPointerEvent, -} from '../test-utils'; +import {createEventTarget, setPointerEvent} from '../testing-library'; let React; let ReactFeatureFlags; @@ -77,9 +70,9 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('does not call callbacks', () => { - const target = ref.current; - dispatchPointerHoverEnter(target); - dispatchPointerHoverExit(target); + const target = createEventTarget(ref.current); + target.pointerenter(); + target.pointerexit(); expect(onHoverChange).not.toBeCalled(); expect(onHoverStart).not.toBeCalled(); expect(onHoverMove).not.toBeCalled(); @@ -103,21 +96,23 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - const target = ref.current; - dispatchPointerHoverEnter(target); + const target = createEventTarget(ref.current); + target.pointerenter(); expect(onHoverStart).toHaveBeenCalledTimes(1); }); it('is not called for touch pointers', () => { - const target = ref.current; - dispatchTouchTap(target); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType: 'touch'}); + target.pointerup({pointerType: 'touch'}); expect(onHoverStart).not.toBeCalled(); }); it('is called if a mouse pointer is used after a touch pointer', () => { - const target = ref.current; - dispatchTouchTap(target); - dispatchPointerHoverEnter(target); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType: 'touch'}); + target.pointerup({pointerType: 'touch'}); + target.pointerenter(); expect(onHoverStart).toHaveBeenCalledTimes(1); }); }); @@ -138,18 +133,19 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - const target = ref.current; - dispatchPointerHoverEnter(target); + const target = createEventTarget(ref.current); + target.pointerenter(); expect(onHoverChange).toHaveBeenCalledTimes(1); expect(onHoverChange).toHaveBeenCalledWith(true); - dispatchPointerHoverExit(target); + target.pointerexit(); expect(onHoverChange).toHaveBeenCalledTimes(2); expect(onHoverChange).toHaveBeenCalledWith(false); }); it('is not called for touch pointers', () => { - const target = ref.current; - dispatchTouchTap(target); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType: 'touch'}); + target.pointerup({pointerType: 'touch'}); expect(onHoverChange).not.toBeCalled(); }); }); @@ -170,31 +166,32 @@ describe.each(table)('Hover responder', hasPointerEvents => { }); it('is called for mouse pointers', () => { - const target = ref.current; - dispatchPointerHoverEnter(target); - dispatchPointerHoverExit(target); + const target = createEventTarget(ref.current); + target.pointerenter(); + target.pointerexit(); expect(onHoverEnd).toHaveBeenCalledTimes(1); }); if (hasPointerEvents) { it('is called once for cancelled mouse pointers', () => { - const target = ref.current; - dispatchPointerHoverEnter(target); - dispatchPointerCancel(target); + const target = createEventTarget(ref.current); + target.pointerenter(); + target.pointercancel(); expect(onHoverEnd).toHaveBeenCalledTimes(1); // only called once if cancel follows exit onHoverEnd.mockReset(); - dispatchPointerHoverEnter(target); - dispatchPointerHoverExit(target); - dispatchPointerCancel(target); + target.pointerenter(); + target.pointerexit(); + target.pointercancel(); expect(onHoverEnd).toHaveBeenCalledTimes(1); }); } it('is not called for touch pointers', () => { - const target = ref.current; - dispatchTouchTap(target); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType: 'touch'}); + target.pointerup({pointerType: 'touch'}); expect(onHoverEnd).not.toBeCalled(); }); }); @@ -211,10 +208,10 @@ describe.each(table)('Hover responder', hasPointerEvents => { }; ReactDOM.render(, container); - const target = ref.current; - dispatchPointerHoverEnter(target); - dispatchPointerHoverMove(target, {x: 0, y: 0}); - dispatchPointerHoverMove(target, {x: 1, y: 1}); + const target = createEventTarget(ref.current); + target.pointerenter(); + target.pointerhover({x: 0, y: 0}); + target.pointerhover({x: 1, y: 1}); expect(onHoverMove).toHaveBeenCalledTimes(2); expect(onHoverMove).toHaveBeenCalledWith( expect.objectContaining({type: 'hovermove'}), @@ -254,15 +251,17 @@ describe.each(table)('Hover responder', hasPointerEvents => { }; ReactDOM.render(, container); - const innerTarget = innerRef.current; - const outerTarget = outerRef.current; + const innerNode = innerRef.current; + const outerNode = outerRef.current; + const innerTarget = createEventTarget(innerNode); + const outerTarget = createEventTarget(outerNode); - dispatchPointerHoverEnter(outerTarget, {relatedTarget: container}); - dispatchPointerHoverExit(outerTarget, {relatedTarget: innerTarget}); - dispatchPointerHoverEnter(innerTarget, {relatedTarget: outerTarget}); - dispatchPointerHoverExit(innerTarget, {relatedTarget: outerTarget}); - dispatchPointerHoverEnter(outerTarget, {relatedTarget: innerTarget}); - dispatchPointerHoverExit(outerTarget, {relatedTarget: container}); + outerTarget.pointerenter({relatedTarget: container}); + outerTarget.pointerexit({relatedTarget: innerNode}); + innerTarget.pointerenter({relatedTarget: outerNode}); + innerTarget.pointerexit({relatedTarget: outerNode}); + outerTarget.pointerenter({relatedTarget: innerNode}); + outerTarget.pointerexit({relatedTarget: container}); expect(events).toEqual([ 'outer: onHoverStart', @@ -315,12 +314,13 @@ describe.each(table)('Hover responder', hasPointerEvents => { }; ReactDOM.render(, container); - const target = ref.current; + const node = ref.current; + const target = createEventTarget(node); - dispatchPointerHoverEnter(target, {x: 10, y: 10}); - dispatchPointerHoverMove(target, {x: 10, y: 10}); - dispatchPointerHoverMove(target, {x: 20, y: 20}); - dispatchPointerHoverExit(target, {x: 20, y: 20}); + target.pointerenter({x: 10, y: 10}); + target.pointerhover({x: 10, y: 10}); + target.pointerhover({x: 20, y: 20}); + target.pointerexit({x: 20, y: 20}); expect(eventLog).toEqual([ { @@ -330,7 +330,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 10, clientX: 10, clientY: 10, - target, + target: node, timeStamp: timeStamps[0], type: 'hoverstart', pointerType: 'mouse', @@ -342,7 +342,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 10, clientX: 10, clientY: 10, - target, + target: node, timeStamp: timeStamps[1], type: 'hovermove', pointerType: 'mouse', @@ -354,7 +354,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 20, clientX: 20, clientY: 20, - target, + target: node, timeStamp: timeStamps[2], type: 'hovermove', pointerType: 'mouse', @@ -366,7 +366,7 @@ describe.each(table)('Hover responder', hasPointerEvents => { pageY: 20, clientX: 20, clientY: 20, - target, + target: node, timeStamp: timeStamps[3], type: 'hoverend', pointerType: 'mouse', diff --git a/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js b/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js index f3a655f79039b..613d1e6cace3f 100644 --- a/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Keyboard-test.internal.js @@ -14,7 +14,7 @@ let ReactFeatureFlags; let ReactDOM; let useKeyboardResponder; -import {keydown, keyup} from '../test-utils'; +import {createEventTarget} from '../testing-library'; function initializeModules(hasPointerEvents) { jest.resetModules(); @@ -59,9 +59,9 @@ describe('Keyboard event responder', () => { }); it('prevents custom events being dispatched', () => { - const target = ref.current; - target.dispatchEvent(keydown()); - target.dispatchEvent(keyup()); + const target = createEventTarget(ref.current); + target.keydown(); + target.keyup(); expect(onKeyDown).not.toBeCalled(); expect(onKeyUp).not.toBeCalled(); }); @@ -83,7 +83,8 @@ describe('Keyboard event responder', () => { }); it('is called after "keydown" event', () => { - ref.current.dispatchEvent(keydown({key: 'Q'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Q'}); expect(onKeyDown).toHaveBeenCalledTimes(1); expect(onKeyDown).toHaveBeenCalledWith( expect.objectContaining({key: 'Q', type: 'keydown'}), @@ -109,9 +110,9 @@ describe('Keyboard event responder', () => { }); it('is called after "keydown" event', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Q'})); - target.dispatchEvent(keyup({key: 'Q'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Q'}); + target.keyup({key: 'Q'}); expect(onKeyDown).toHaveBeenCalledTimes(1); expect(onKeyDown).toHaveBeenCalledWith( expect.objectContaining({key: 'Q', type: 'keydown'}), diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index a6a42a5d011a3..10ddd2af356ca 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -9,27 +9,13 @@ 'use strict'; -import { - click, - dispatchPointerCancel, - dispatchPointerDown, - dispatchPointerUp, - dispatchPointerHoverMove, - dispatchPointerMove, - keydown, - keyup, - scroll, - pointerdown, - pointerup, - setPointerEvent, -} from '../test-utils'; +import {createEventTarget, setPointerEvent} from '../testing-library'; let React; let ReactFeatureFlags; let ReactDOM; let PressResponder; let usePressResponder; -let Scheduler; function initializeModules(hasPointerEvents) { jest.resetModules(); @@ -40,7 +26,6 @@ function initializeModules(hasPointerEvents) { ReactDOM = require('react-dom'); PressResponder = require('react-events/press').PressResponder; usePressResponder = require('react-events/press').usePressResponder; - Scheduler = require('scheduler'); } function removePressMoveStrings(eventString) { @@ -92,9 +77,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('does not call callbacks', () => { - const target = ref.current; - dispatchPointerDown(target); - dispatchPointerUp(target); + const target = createEventTarget(ref.current); + target.pointerdown(); + target.pointerup(); expect(onPressStart).not.toBeCalled(); expect(onPress).not.toBeCalled(); expect(onPressEnd).not.toBeCalled(); @@ -120,7 +105,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it.each(pointerTypesTable)( 'is called after pointer down: %s', pointerType => { - dispatchPointerDown(ref.current, {pointerType}); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType, type: 'pressstart'}), @@ -129,7 +115,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { ); it('is called after auxillary-button pointer down', () => { - dispatchPointerDown(ref.current, {button: 1, pointerType: 'mouse'}); + const target = createEventTarget(ref.current); + target.pointerdown({button: 1, pointerType: 'mouse'}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -140,39 +127,31 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { ); }); - it('is not called after "pointermove" following auxillary-button press', () => { - const target = ref.current; - target.getBoundingClientRect = () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }); - dispatchPointerDown(target, { - button: 1, - pointerType: 'mouse', - }); - dispatchPointerUp(target, { - button: 1, - pointerType: 'mouse', - }); - dispatchPointerHoverMove(target, {x: 110, y: 110}); - dispatchPointerHoverMove(target, {x: 50, y: 50}); + it('is not called after pointer move following auxillary-button press', () => { + const node = ref.current; + const target = createEventTarget(node); + target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100}); + target.pointerdown({button: 1, pointerType: 'mouse'}); + target.pointerup({button: 1, pointerType: 'mouse'}); + target.pointerhover({x: 110, y: 110}); + target.pointerhover({x: 50, y: 50}); expect(onPressStart).toHaveBeenCalledTimes(1); }); it('ignores any events not caused by primary/auxillary-click or touch/pen contact', () => { - const target = ref.current; - dispatchPointerDown(target, {button: 2}); - dispatchPointerDown(target, {button: 5}); + const target = createEventTarget(ref.current); + target.pointerdown({button: 2}); + target.pointerup({button: 2}); + target.pointerdown({button: 5}); + target.pointerup({button: 5}); expect(onPressStart).toHaveBeenCalledTimes(0); }); it('is called once after "keydown" events for Enter', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent(keydown({key: 'Enter'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); + target.keydown({key: 'Enter'}); + target.keydown({key: 'Enter'}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), @@ -180,11 +159,11 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('is called once after "keydown" events for Spacebar', () => { - const target = ref.current; + const target = createEventTarget(ref.current); const preventDefault = jest.fn(); - target.dispatchEvent(keydown({key: ' ', preventDefault})); + target.keydown({key: ' ', preventDefault}); expect(preventDefault).toBeCalled(); - target.dispatchEvent(keydown({key: ' ', preventDefault})); + target.keydown({key: ' ', preventDefault}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -195,7 +174,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('is not called after "keydown" for other keys', () => { - ref.current.dispatchEvent(keydown({key: 'a'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'a'}); expect(onPressStart).not.toBeCalled(); }); }); @@ -219,9 +199,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it.each(pointerTypesTable)( 'is called after pointer up: %s', pointerType => { - const target = ref.current; - dispatchPointerDown(target, {pointerType}); - dispatchPointerUp(target, {pointerType}); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType}); + target.pointerup({pointerType}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType, type: 'pressend'}), @@ -230,9 +210,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { ); it('is called after auxillary-button pointer up', () => { - const target = ref.current; - dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); - dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); + const target = createEventTarget(ref.current); + target.pointerdown({button: 1, pointerType: 'mouse'}); + target.pointerup({button: 1, pointerType: 'mouse'}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({ @@ -244,11 +224,11 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('is called after "keyup" event for Enter', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Enter'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); // click occurs before keyup - target.dispatchEvent(click()); - target.dispatchEvent(keyup({key: 'Enter'})); + target.click(); + target.keyup({key: 'Enter'}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -256,9 +236,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('is called after "keyup" event for Spacebar', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: ' '})); - target.dispatchEvent(keyup({key: ' '})); + const target = createEventTarget(ref.current); + target.keydown({key: ' '}); + target.keyup({key: ' '}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -266,24 +246,22 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { }); it('is not called after "keyup" event for other keys', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent(keyup({key: 'a'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); + target.keyup({key: 'a'}); expect(onPressEnd).not.toBeCalled(); }); it('is called with keyboard modifiers', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent( - keyup({ - key: 'Enter', - metaKey: true, - ctrlKey: true, - altKey: true, - shiftKey: true, - }), - ); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); + target.keyup({ + key: 'Enter', + metaKey: true, + ctrlKey: true, + altKey: true, + shiftKey: true, + }); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({ pointerType: 'keyboard', @@ -316,21 +294,22 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it.each(pointerTypesTable)( 'is called after pointer down and up: %s', pointerType => { - const target = ref.current; - dispatchPointerDown(target, {pointerType}); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType}); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - dispatchPointerUp(target, {pointerType}); + target.pointerup({pointerType}); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }, ); it('is called after valid "keydown" and "keyup" events', () => { - ref.current.dispatchEvent(keydown({key: 'Enter'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(keyup({key: 'Enter'})); + target.keyup({key: 'Enter'}); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); @@ -361,9 +340,9 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it.each(pointerTypesTable)( 'is called after pointer up: %s', pointerType => { - const target = ref.current; - dispatchPointerDown(target, {pointerType}); - dispatchPointerUp(target, {pointerType, x: 10, y: 10}); + const target = createEventTarget(ref.current); + target.pointerdown({pointerType}); + target.pointerup({pointerType, x: 10, y: 10}); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({pointerType, type: 'press'}), @@ -372,16 +351,16 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { ); it('is not called after auxillary-button press', () => { - const target = ref.current; - dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); - dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); + const target = createEventTarget(ref.current); + target.pointerdown({button: 1, pointerType: 'mouse'}); + target.pointerup({button: 1, pointerType: 'mouse'}); expect(onPress).not.toHaveBeenCalled(); }); it('is called after valid "keyup" event', () => { - const target = ref.current; - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent(keyup({key: 'Enter'})); + const target = createEventTarget(ref.current); + target.keydown({key: 'Enter'}); + target.keyup({key: 'Enter'}); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'press'}), @@ -391,27 +370,22 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { it('is not called after invalid "keyup" event', () => { const inputRef = React.createRef(); const Component = () => { - const listener = usePressResponder({ - onPress, - }); + const listener = usePressResponder({onPress}); return ; }; ReactDOM.render(, container); - const target = inputRef.current; - target.dispatchEvent(keydown({key: 'Enter'})); - target.dispatchEvent(keyup({key: 'Enter'})); - target.dispatchEvent(keydown({key: ' '})); - target.dispatchEvent(keyup({key: ' '})); + const target = createEventTarget(inputRef.current); + target.keydown({key: 'Enter'}); + target.keyup({key: 'Enter'}); + target.keydown({key: ' '}); + target.keyup({key: ' '}); expect(onPress).not.toBeCalled(); }); it('is called with modifier keys', () => { - const target = ref.current; - dispatchPointerDown(target, {metaKey: true, pointerType: 'mouse'}); - dispatchPointerUp(target, { - metaKey: true, - pointerType: 'mouse', - }); + const target = createEventTarget(ref.current); + target.pointerdown({metaKey: true, pointerType: 'mouse'}); + target.pointerup({metaKey: true, pointerType: 'mouse'}); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({ pointerType: 'mouse', @@ -426,9 +400,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { const divRef = React.createRef(); const Component = () => { - const listener = usePressResponder({ - onPress, - }); + const listener = usePressResponder({onPress}); return (