From e12896923083cc7978298b6cf026e66430b1dd3c Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sat, 30 Mar 2019 13:46:01 -0700 Subject: [PATCH] Add Press responder event tests Behavior being tested takes cues from React Native's Pressability. A couple of these tests fail and require the Press implementation to be patched. Ref #15257 --- .../src/__tests__/Press-test.internal.js | 409 +++++++++++++++--- 1 file changed, 338 insertions(+), 71 deletions(-) diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index 7ecdc3acb8671..1483e089ec5db 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -14,7 +14,23 @@ let ReactFeatureFlags; let ReactDOM; let Press; -describe('Press event responder', () => { +const DEFAULT_LONG_PRESS_DELAY = 1000; + +const createPointerEvent = type => { + const event = document.createEvent('Event'); + event.initEvent(type, true, true); + return event; +}; + +const createKeyboardEvent = (type, data) => { + return new KeyboardEvent(type, { + bubbles: true, + cancelable: true, + ...data, + }); +}; + +describe('Event responder: Press', () => { let container; beforeEach(() => { @@ -34,95 +50,346 @@ describe('Press event responder', () => { container = null; }); - it('should support onPress', () => { - let buttonRef = React.createRef(); - let events = []; - - function handleOnPress1() { - events.push('press 1'); - } - - function handleOnPress2() { - events.push('press 2'); - } - - function handleOnMouseDown() { - events.push('mousedown'); - } - - function handleKeyDown() { - events.push('keydown'); - } - - function Component() { - return ( - - - - + describe('onPressStart', () => { + let onPressStart, ref; + + beforeEach(() => { + onPressStart = jest.fn(); + ref = React.createRef(); + const element = ( + +
); - } + ReactDOM.render(element, container); + }); - ReactDOM.render(, container); + it('is called after "pointerdown" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); - const mouseDownEvent = document.createEvent('Event'); - mouseDownEvent.initEvent('mousedown', true, true); - buttonRef.current.dispatchEvent(mouseDownEvent); + it('ignores emulated "mousedown" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('mousedown')); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); - const mouseUpEvent = document.createEvent('Event'); - mouseUpEvent.initEvent('mouseup', true, true); - buttonRef.current.dispatchEvent(mouseUpEvent); + // No PointerEvent fallbacks + it('is called after "mousedown" event', () => { + ref.current.dispatchEvent(createPointerEvent('mousedown')); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); + it('is called after "touchstart" event', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + expect(onPressStart).toHaveBeenCalledTimes(1); + }); + + // TODO: complete delayPressStart tests + // describe('delayPressStart', () => {}); + }); - expect(events).toEqual(['mousedown', 'press 2', 'press 1']); + describe('onPressEnd', () => { + let onPressEnd, ref; + + beforeEach(() => { + onPressEnd = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called after "pointerup" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); - events = []; - const keyDownEvent = new KeyboardEvent('keydown', { - key: 'Enter', - bubbles: true, - cancelable: true, + it('ignores emulated "mouseup" event', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createPointerEvent('touchend')); + ref.current.dispatchEvent(createPointerEvent('mouseup')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + + // No PointerEvent fallbacks + it('is called after "mouseup" event', () => { + ref.current.dispatchEvent(createPointerEvent('mousedown')); + ref.current.dispatchEvent(createPointerEvent('mouseup')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + + it('is called after "touchend" event', () => { + ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createPointerEvent('touchend')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + + // TODO: complete delayPressStart tests + // describe('delayPressStart', () => {}); + }); + + describe('onPressChange', () => { + let onPressChange, ref; + + beforeEach(() => { + onPressChange = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called after "pointerdown" and "pointerup" events', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }); + }); + + describe('onPress', () => { + let onPress, ref; + + beforeEach(() => { + onPress = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); }); - buttonRef.current.dispatchEvent(keyDownEvent); - // press 1 should not occur as press 2 will preventDefault - expect(events).toEqual(['keydown', 'press 2']); + it('is called after "pointerup" event', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPress).toHaveBeenCalledTimes(1); + }); }); - it('should support onPressStart and onPressEnd', () => { - let divRef = React.createRef(); - let events = []; + describe('onLongPress', () => { + let onLongPress, ref; + + beforeEach(() => { + onLongPress = jest.fn(); + ref = React.createRef(); + const element = ( + +
+ + ); + ReactDOM.render(element, container); + }); + + it('is called if press lasts default delay', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); + expect(onLongPress).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onLongPress).toHaveBeenCalledTimes(1); + }); - function handleOnPressStart() { - events.push('onPressStart'); - } + it('is not called if press is released before delay', () => { + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + jest.advanceTimersByTime(1); + expect(onLongPress).not.toBeCalled(); + }); - function handleOnPressEnd() { - events.push('onPressEnd'); - } + describe('delayLongPress', () => { + it('can be configured', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(1999); + expect(onLongPress).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onLongPress).toHaveBeenCalledTimes(1); + }); + + it('uses 10ms minimum delay length', () => { + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(9); + expect(onLongPress).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onLongPress).toHaveBeenCalledTimes(1); + }); + + /* + it('compounds with "delayPressStart"', () => { + const delayPressStart = 100; + const element = ( + +
+ + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1); + expect(onLongPress).not.toBeCalled(); + jest.advanceTimersByTime(1); + expect(onLongPress).toHaveBeenCalledTimes(1); + }); + */ + }); + }); - function Component() { - return ( - -
Press me!
+ describe('onLongPressChange', () => { + it('is called when long press state changes', () => { + const onLongPressChange = jest.fn(); + const ref = React.createRef(); + const element = ( + +
); - } + ReactDOM.render(element, container); - ReactDOM.render(, container); + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); + expect(onLongPressChange).toHaveBeenCalledTimes(1); + expect(onLongPressChange).toHaveBeenCalledWith(true); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onLongPressChange).toHaveBeenCalledTimes(2); + expect(onLongPressChange).toHaveBeenCalledWith(false); + }); + }); + + describe('onLongPressShouldCancelPress', () => { + it('if true it cancels "onPress"', () => { + const onPress = jest.fn(); + const onPressChange = jest.fn(); + const ref = React.createRef(); + const element = ( + {}} + onLongPressShouldCancelPress={() => true} + onPressChange={onPressChange} + onPress={onPress}> +
+ + ); + ReactDOM.render(element, container); + + // NOTE: onPressChange behavior should not be affected + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + expect(onPressChange).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(onPress).not.toBeCalled(); + expect(onPressChange).toHaveBeenCalledTimes(2); + }); + }); + + // TODO + //describe('`onPress*` with movement', () => { + //describe('within bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect X │ <= Move to X and release + * └──────────────────┘ + */ - const pointerEnterEvent = document.createEvent('Event'); - pointerEnterEvent.initEvent('pointerdown', true, true); - divRef.current.dispatchEvent(pointerEnterEvent); + //it('"onPress*" events are called when no delay', () => {}); + //it('"onPress*" events are called after a delay', () => {}); + //}); - const pointerLeaveEvent = document.createEvent('Event'); - pointerLeaveEvent.initEvent('pointerup', true, true); - divRef.current.dispatchEvent(pointerLeaveEvent); + //describe('beyond bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect │ + * └──────────────────┘ + * X <= Move to X and release + */ - expect(events).toEqual(['onPressStart', 'onPressEnd']); + //it('"onPress" only is not called when no delay', () => {}); + //it('"onPress*" events are not called after a delay', () => {}); + //it('"onPress*" events are called when press is released before measure completes', () => {}); + //}); + //}); + + describe('nested responders', () => { + it('dispatch events in the correct order', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + + +
+ + + ); + + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointerup')); + expect(events).toEqual([ + 'pointerdown', + 'inner: onPressStart', + 'inner: onPressChange', + 'outer: onPressStart', + 'outer: onPressChange', + 'pointerup', + 'inner: onPressEnd', + 'inner: onPressChange', + 'inner: onPress', + 'outer: onPressEnd', + 'outer: onPressChange', + 'outer: onPress', + ]); + + events = []; + ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + // Outer press should not occur as inner press will preventDefault + expect(events).toEqual(['keydown', 'inner: onPress']); + }); }); });