From b41568be08ecc19162224b10ba68959e83e81490 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 6 Apr 2023 13:05:31 +0200 Subject: [PATCH 01/44] feat: create first version of userEvent.press --- src/__tests__/userEvent.test.tsx | 25 +++++++++++++++++++++++++ src/pure.ts | 1 + src/userEvent/touchEvents.ts | 20 ++++++++++++++++++++ src/userEvent/userEvent.ts | 5 +++++ 4 files changed, 51 insertions(+) create mode 100644 src/__tests__/userEvent.test.tsx create mode 100644 src/userEvent/touchEvents.ts create mode 100644 src/userEvent/userEvent.ts diff --git a/src/__tests__/userEvent.test.tsx b/src/__tests__/userEvent.test.tsx new file mode 100644 index 000000000..3e8f1c5f7 --- /dev/null +++ b/src/__tests__/userEvent.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Pressable } from 'react-native'; +import { render, screen, userEvent } from '../pure'; + +jest.useFakeTimers(); + +describe('userEvent.press', () => { + test('calls onPress prop of touchable', () => { + const mockOnPress = jest.fn(); + + render(); + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('does not call press when pressable is disabled', () => { + const mockOnPress = jest.fn(); + + render(); + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/pure.ts b/src/pure.ts index 2996c6cd1..d54956222 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -14,6 +14,7 @@ export { export { getDefaultNormalizer } from './matches'; export { renderHook } from './renderHook'; export { screen } from './screen'; +export { userEvent } from './userEvent/userEvent'; export type { RenderOptions, diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts new file mode 100644 index 000000000..d73d4221a --- /dev/null +++ b/src/userEvent/touchEvents.ts @@ -0,0 +1,20 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import act from '../act'; + +const defaultPressEvent = { + persist: jest.fn(), + nativeEvent: { + timestamp: new Date().getTime(), + }, + currentTarget: { measure: jest.fn() }, +}; + +export const press = (element: ReactTestInstance) => { + act(() => { + if (element.props.onStartShouldSetResponder()) { + element.props.onResponderGrant(defaultPressEvent); + element.props.onResponderRelease(defaultPressEvent); + jest.runOnlyPendingTimers(); + } + }); +}; diff --git a/src/userEvent/userEvent.ts b/src/userEvent/userEvent.ts new file mode 100644 index 000000000..84e47b46d --- /dev/null +++ b/src/userEvent/userEvent.ts @@ -0,0 +1,5 @@ +import { press } from './touchEvents'; + +export const userEvent = { + press, +}; From 65ba7db5e10bd76a0bc1ea68c48db27c5418f6f9 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 6 Apr 2023 13:44:24 +0200 Subject: [PATCH 02/44] feat: do not trigger press event when pointer events is disabled --- src/__tests__/userEvent.test.tsx | 61 ++++++++++++++++++- .../isHostElementPointerEventEnabled.ts | 20 ++++++ src/userEvent/touchEvents.ts | 6 +- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/helpers/isHostElementPointerEventEnabled.ts diff --git a/src/__tests__/userEvent.test.tsx b/src/__tests__/userEvent.test.tsx index 3e8f1c5f7..68ee4028a 100644 --- a/src/__tests__/userEvent.test.tsx +++ b/src/__tests__/userEvent.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Pressable } from 'react-native'; +import { Pressable, View } from 'react-native'; import { render, screen, userEvent } from '../pure'; jest.useFakeTimers(); @@ -22,4 +22,63 @@ describe('userEvent.press', () => { expect(mockOnPress).not.toHaveBeenCalled(); }); + + test('does not call press when pointer events is none', () => { + const mockOnPress = jest.fn(); + + render( + + ); + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not call press when pointer events is box-none', () => { + const mockOnPress = jest.fn(); + + render( + + ); + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not call press when parent has pointer events box-only', () => { + const mockOnPress = jest.fn(); + + render( + + + + ); + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('calls press when pressable has pointer events box-only', () => { + const mockOnPress = jest.fn(); + + render( + + ); + + userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).toHaveBeenCalled(); + }); }); diff --git a/src/helpers/isHostElementPointerEventEnabled.ts b/src/helpers/isHostElementPointerEventEnabled.ts new file mode 100644 index 000000000..a863fcfc8 --- /dev/null +++ b/src/helpers/isHostElementPointerEventEnabled.ts @@ -0,0 +1,20 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { getHostParent } from './component-tree'; + +export const isHostElementPointerEventEnabled = ( + element: ReactTestInstance, + isParent?: boolean +): boolean => { + const parentCondition = isParent + ? element?.props.pointerEvents === 'box-only' + : element?.props.pointerEvents === 'box-none'; + + if (element?.props.pointerEvents === 'none' || parentCondition) { + return false; + } + + const hostParent = getHostParent(element); + if (!hostParent) return true; + + return isHostElementPointerEventEnabled(hostParent, true); +}; diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index d73d4221a..3170475b1 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -1,5 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from '../act'; +import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; const defaultPressEvent = { persist: jest.fn(), @@ -11,7 +12,10 @@ const defaultPressEvent = { export const press = (element: ReactTestInstance) => { act(() => { - if (element.props.onStartShouldSetResponder()) { + if ( + isHostElementPointerEventEnabled(element) && + element.props.onStartShouldSetResponder() + ) { element.props.onResponderGrant(defaultPressEvent); element.props.onResponderRelease(defaultPressEvent); jest.runOnlyPendingTimers(); From ef041a0c9c0b5bff606676718153828b64a5e187 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 6 Apr 2023 13:50:53 +0200 Subject: [PATCH 03/44] feat: make press events bubble up when trigerring a non touch responder --- src/__tests__/userEvent.test.tsx | 15 +++++++++++++- src/userEvent/touchEvents.ts | 34 ++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/__tests__/userEvent.test.tsx b/src/__tests__/userEvent.test.tsx index 68ee4028a..b6ea7b783 100644 --- a/src/__tests__/userEvent.test.tsx +++ b/src/__tests__/userEvent.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Pressable, View } from 'react-native'; +import { Pressable, Text, View } from 'react-native'; import { render, screen, userEvent } from '../pure'; jest.useFakeTimers(); @@ -81,4 +81,17 @@ describe('userEvent.press', () => { expect(mockOnPress).toHaveBeenCalled(); }); + + test('crawls up in the tree to find an element that responds to touch events', () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); }); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 3170475b1..0b23f72eb 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -1,5 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from '../act'; +import { getHostParent } from '../helpers/component-tree'; import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; const defaultPressEvent = { @@ -11,14 +12,31 @@ const defaultPressEvent = { }; export const press = (element: ReactTestInstance) => { + if (isEnabledTouchResponder(element)) { + triggerPressEvent(element); + return; + } + + const hostParentElement = getHostParent(element); + if (!hostParentElement) { + return; + } + + press(hostParentElement); +}; + +const triggerPressEvent = (element: ReactTestInstance) => { act(() => { - if ( - isHostElementPointerEventEnabled(element) && - element.props.onStartShouldSetResponder() - ) { - element.props.onResponderGrant(defaultPressEvent); - element.props.onResponderRelease(defaultPressEvent); - jest.runOnlyPendingTimers(); - } + element.props.onResponderGrant(defaultPressEvent); + element.props.onResponderRelease(defaultPressEvent); + jest.runOnlyPendingTimers(); }); }; + +const isEnabledTouchResponder = (element: ReactTestInstance) => { + return ( + isHostElementPointerEventEnabled(element) && + element.props.onStartShouldSetResponder && + element.props.onStartShouldSetResponder() + ); +}; From f011f7c7e434495471d53128c444e82f8a361758 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Fri, 7 Apr 2023 14:54:19 +0200 Subject: [PATCH 04/44] tests: add test cases for userevent.press to check calls to onPressIn and onPressOut --- src/__tests__/userEvent.test.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/__tests__/userEvent.test.tsx b/src/__tests__/userEvent.test.tsx index b6ea7b783..8c4343563 100644 --- a/src/__tests__/userEvent.test.tsx +++ b/src/__tests__/userEvent.test.tsx @@ -94,4 +94,30 @@ describe('userEvent.press', () => { expect(mockOnPress).toHaveBeenCalled(); }); + + test('calls onPressIn', () => { + const mockOnPressIn = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPressIn).toHaveBeenCalled(); + }); + + test('calls onPressOut', () => { + const mockOnPressOut = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPressOut).toHaveBeenCalled(); + }); }); From f8e4d3e0fb2a706f5ef431a31b60e3bdbddb38e1 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 13 Apr 2023 12:27:41 +0200 Subject: [PATCH 05/44] refactor: move userEvent.press tests in a dedicated test file --- .../{userEvent.test.tsx => userEvent/userEvent.press.test.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/__tests__/{userEvent.test.tsx => userEvent/userEvent.press.test.tsx} (98%) diff --git a/src/__tests__/userEvent.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx similarity index 98% rename from src/__tests__/userEvent.test.tsx rename to src/__tests__/userEvent/userEvent.press.test.tsx index 8c4343563..5327ebeac 100644 --- a/src/__tests__/userEvent.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Pressable, Text, View } from 'react-native'; -import { render, screen, userEvent } from '../pure'; +import { render, screen, userEvent } from '../../pure'; jest.useFakeTimers(); From 6e69ba55ea2c486753955ef2c6aaba7b49b2b0db Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 13 Apr 2023 12:38:51 +0200 Subject: [PATCH 06/44] feat: add pressDuration option for userEvent.press --- .../userEvent/userEvent.press.test.tsx | 65 +++++++++++++++++++ src/pure.ts | 1 + src/userEvent/touchEvents.ts | 19 ++++-- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 5327ebeac..80217c69f 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -120,4 +120,69 @@ describe('userEvent.press', () => { expect(mockOnPressOut).toHaveBeenCalled(); }); + + test('does not call onLongPress for pressDuration of 0', () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + }); + + test('calls onLongPress for a duration greater than default onLongPressDelay', () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); + + test('calls onLongPress when duration is greater than specified onLongPressDelay', () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + + userEvent.press(screen.getByText('press me'), { pressDuration: 1000 }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + }); + + test('does not calls onPress when onLongPress is called', () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); }); diff --git a/src/pure.ts b/src/pure.ts index d54956222..305296779 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -23,3 +23,4 @@ export type { } from './render'; export type { RenderHookOptions, RenderHookResult } from './renderHook'; export type { Config } from './config'; +export type { PressOptions } from './userEvent/touchEvents'; diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 0b23f72eb..e462f89e8 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -11,9 +11,16 @@ const defaultPressEvent = { currentTarget: { measure: jest.fn() }, }; -export const press = (element: ReactTestInstance) => { +export type PressOptions = { + pressDuration: number; +}; + +export const press = ( + element: ReactTestInstance, + options: PressOptions = { pressDuration: 0 } +) => { if (isEnabledTouchResponder(element)) { - triggerPressEvent(element); + triggerPressEvent(element, options); return; } @@ -22,12 +29,16 @@ export const press = (element: ReactTestInstance) => { return; } - press(hostParentElement); + press(hostParentElement, options); }; -const triggerPressEvent = (element: ReactTestInstance) => { +const triggerPressEvent = ( + element: ReactTestInstance, + options: PressOptions = { pressDuration: 0 } +) => { act(() => { element.props.onResponderGrant(defaultPressEvent); + jest.advanceTimersByTime(options.pressDuration); element.props.onResponderRelease(defaultPressEvent); jest.runOnlyPendingTimers(); }); From 8aaa686495379e011108a7e75839c6a00061ee8b Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 20 Apr 2023 12:20:23 +0200 Subject: [PATCH 07/44] refactor: group test that check prop calls --- .../userEvent/userEvent.press.test.tsx | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 80217c69f..008464261 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -5,13 +5,24 @@ import { render, screen, userEvent } from '../../pure'; jest.useFakeTimers(); describe('userEvent.press', () => { - test('calls onPress prop of touchable', () => { + test('calls onPressIn, onPress and onPressOut prop of touchable', () => { const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); - render(); + render( + + ); userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).toHaveBeenCalled(); + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); }); test('does not call press when pressable is disabled', () => { @@ -95,32 +106,6 @@ describe('userEvent.press', () => { expect(mockOnPress).toHaveBeenCalled(); }); - test('calls onPressIn', () => { - const mockOnPressIn = jest.fn(); - - render( - - press me - - ); - userEvent.press(screen.getByText('press me')); - - expect(mockOnPressIn).toHaveBeenCalled(); - }); - - test('calls onPressOut', () => { - const mockOnPressOut = jest.fn(); - - render( - - press me - - ); - userEvent.press(screen.getByText('press me')); - - expect(mockOnPressOut).toHaveBeenCalled(); - }); - test('does not call onLongPress for pressDuration of 0', () => { const mockOnLongPress = jest.fn(); From 5e506f34817e866cf9705718200f0507dedc4ee6 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 20 Apr 2023 12:27:07 +0200 Subject: [PATCH 08/44] feat: add support for touchable opacity for userevent press --- src/__tests__/userEvent/userEvent.press.test.tsx | 15 ++++++++++++++- src/userEvent/touchEvents.ts | 10 ++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 008464261..388976c3e 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Pressable, Text, View } from 'react-native'; +import { Pressable, Text, TouchableOpacity, View } from 'react-native'; import { render, screen, userEvent } from '../../pure'; jest.useFakeTimers(); @@ -170,4 +170,17 @@ describe('userEvent.press', () => { expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); }); + + test('works on TouchableOpacity', () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); }); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index e462f89e8..0c6a84e71 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -37,9 +37,15 @@ const triggerPressEvent = ( options: PressOptions = { pressDuration: 0 } ) => { act(() => { - element.props.onResponderGrant(defaultPressEvent); + element.props.onResponderGrant({ + ...defaultPressEvent, + dispatchConfig: { registrationName: 'onResponderGrant' }, + }); jest.advanceTimersByTime(options.pressDuration); - element.props.onResponderRelease(defaultPressEvent); + element.props.onResponderRelease({ + ...defaultPressEvent, + dispatchConfig: { registrationName: 'onResponderRelease' }, + }); jest.runOnlyPendingTimers(); }); }; From 28014d0341fe984ec3b9fc85451b9bfc7712f8a3 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 20 Apr 2023 12:51:50 +0200 Subject: [PATCH 09/44] feat: add support for Text --- .../userEvent/userEvent.press.test.tsx | 64 ++++++++++++++++++- src/userEvent/touchEvents.ts | 17 +++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 388976c3e..762f3bf3a 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { Pressable, Text, TouchableOpacity, View } from 'react-native'; +import { + Pressable, + Text, + TouchableHighlight, + TouchableOpacity, + View, +} from 'react-native'; import { render, screen, userEvent } from '../../pure'; jest.useFakeTimers(); @@ -183,4 +189,60 @@ describe('userEvent.press', () => { expect(mockOnPress).toHaveBeenCalled(); }); + + test('works on TouchableHighlight', () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on Text', () => { + const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); + }); + + test('doesnt trigger on disabled Text', () => { + const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + press me + + ); + userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).not.toHaveBeenCalled(); + expect(mockOnPressIn).not.toHaveBeenCalled(); + expect(mockOnPressOut).not.toHaveBeenCalled(); + }); }); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 0c6a84e71..9f86a192e 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -1,6 +1,7 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from '../act'; import { getHostParent } from '../helpers/component-tree'; +import { filterNodeByType } from '../helpers/filterNodeByType'; import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; const defaultPressEvent = { @@ -19,6 +20,22 @@ export const press = ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } ) => { + // Text component is mocked in React Native preset so the mock + // doesn't implement the pressability class + // Thus we need to call the onPress prop directly on the host component + if (filterNodeByType(element, 'Text') && !element.props.disabled) { + const { onPressIn, onPress, onPressOut } = element.props; + if (onPressIn) { + onPressIn(defaultPressEvent); + } + if (onPress) { + onPress(defaultPressEvent); + } + if (onPressOut) { + onPressOut(defaultPressEvent); + } + } + if (isEnabledTouchResponder(element)) { triggerPressEvent(element, options); return; From e17f2ddc41e8050db19b9c3bc5719e2690aefb5b Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 27 Apr 2023 12:09:44 +0200 Subject: [PATCH 10/44] feat: add support for TextInput for userEvent.press --- .../userEvent/userEvent.press.test.tsx | 36 +++++++++++++++++++ src/userEvent/touchEvents.ts | 14 ++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 762f3bf3a..6ac8d7966 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Pressable, Text, + TextInput, TouchableHighlight, TouchableOpacity, View, @@ -245,4 +246,39 @@ describe('userEvent.press', () => { expect(mockOnPressIn).not.toHaveBeenCalled(); expect(mockOnPressOut).not.toHaveBeenCalled(); }); + + test('works on TetInput', () => { + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + ); + userEvent.press(screen.getByPlaceholderText('email')); + + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); + }); + + test('does not call onPressIn and onPressOut on non editable TetInput', () => { + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + ); + userEvent.press(screen.getByPlaceholderText('email')); + + expect(mockOnPressIn).not.toHaveBeenCalled(); + expect(mockOnPressOut).not.toHaveBeenCalled(); + }); }); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 9f86a192e..c364df238 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -3,6 +3,7 @@ import act from '../act'; import { getHostParent } from '../helpers/component-tree'; import { filterNodeByType } from '../helpers/filterNodeByType'; import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; +import { getHostComponentNames } from '../helpers/host-component-names'; const defaultPressEvent = { persist: jest.fn(), @@ -20,10 +21,17 @@ export const press = ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } ) => { - // Text component is mocked in React Native preset so the mock + // Text and TextInput components are mocked in React Native preset so the mock // doesn't implement the pressability class - // Thus we need to call the onPress prop directly on the host component - if (filterNodeByType(element, 'Text') && !element.props.disabled) { + // Thus we need to call the props directly on the host component + const isEnabledHostText = + filterNodeByType(element, getHostComponentNames().text) && + !element.props.disabled; + const isEnabledTextInput = + filterNodeByType(element, getHostComponentNames().textInput) && + element.props.editable !== false; + + if (isEnabledHostText || isEnabledTextInput) { const { onPressIn, onPress, onPressOut } = element.props; if (onPressIn) { onPressIn(defaultPressEvent); From 8fe1fb346b8643e047e235b900d1798be87fd73e Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 27 Apr 2023 12:13:52 +0200 Subject: [PATCH 11/44] refactor: add some comments to explain pointer events api --- src/helpers/isHostElementPointerEventEnabled.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/isHostElementPointerEventEnabled.ts b/src/helpers/isHostElementPointerEventEnabled.ts index a863fcfc8..6fe378c54 100644 --- a/src/helpers/isHostElementPointerEventEnabled.ts +++ b/src/helpers/isHostElementPointerEventEnabled.ts @@ -1,6 +1,12 @@ import { ReactTestInstance } from 'react-test-renderer'; import { getHostParent } from './component-tree'; +// pointerEvents controls whether the View can be the target of touch events. +// 'auto': The View can be the target of touch events. +// 'none': The View is never the target of touch events. +// 'box-none': The View is never the target of touch events but its subviews can be +// 'box-only': The view can be the target of touch events but its subviews cannot be +// see the official react native doc https://reactnative.dev/docs/view#pointerevents export const isHostElementPointerEventEnabled = ( element: ReactTestInstance, isParent?: boolean From f5bb7b695ceef3070e297f764f07f077a389ad41 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 27 Apr 2023 12:18:03 +0200 Subject: [PATCH 12/44] refactor: change the api of userEvent.press to make it async --- .../userEvent/userEvent.press.test.tsx | 72 ++++++++++--------- src/userEvent/touchEvents.ts | 4 +- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 6ac8d7966..203d1dfb0 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -12,7 +12,7 @@ import { render, screen, userEvent } from '../../pure'; jest.useFakeTimers(); describe('userEvent.press', () => { - test('calls onPressIn, onPress and onPressOut prop of touchable', () => { + test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { const mockOnPress = jest.fn(); const mockOnPressIn = jest.fn(); const mockOnPressOut = jest.fn(); @@ -25,23 +25,23 @@ describe('userEvent.press', () => { testID="pressable" /> ); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).toHaveBeenCalled(); expect(mockOnPressIn).toHaveBeenCalled(); expect(mockOnPressOut).toHaveBeenCalled(); }); - test('does not call press when pressable is disabled', () => { + test('does not call press when pressable is disabled', async () => { const mockOnPress = jest.fn(); render(); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).not.toHaveBeenCalled(); }); - test('does not call press when pointer events is none', () => { + test('does not call press when pointer events is none', async () => { const mockOnPress = jest.fn(); render( @@ -51,12 +51,12 @@ describe('userEvent.press', () => { testID="pressable" /> ); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).not.toHaveBeenCalled(); }); - test('does not call press when pointer events is box-none', () => { + test('does not call press when pointer events is box-none', async () => { const mockOnPress = jest.fn(); render( @@ -66,12 +66,12 @@ describe('userEvent.press', () => { testID="pressable" /> ); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).not.toHaveBeenCalled(); }); - test('does not call press when parent has pointer events box-only', () => { + test('does not call press when parent has pointer events box-only', async () => { const mockOnPress = jest.fn(); render( @@ -79,12 +79,12 @@ describe('userEvent.press', () => { ); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).not.toHaveBeenCalled(); }); - test('calls press when pressable has pointer events box-only', () => { + test('calls press when pressable has pointer events box-only', async () => { const mockOnPress = jest.fn(); render( @@ -95,12 +95,12 @@ describe('userEvent.press', () => { /> ); - userEvent.press(screen.getByTestId('pressable')); + await userEvent.press(screen.getByTestId('pressable')); expect(mockOnPress).toHaveBeenCalled(); }); - test('crawls up in the tree to find an element that responds to touch events', () => { + test('crawls up in the tree to find an element that responds to touch events', async () => { const mockOnPress = jest.fn(); render( @@ -108,12 +108,12 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnPress).toHaveBeenCalled(); }); - test('does not call onLongPress for pressDuration of 0', () => { + test('does not call onLongPress for pressDuration of 0', async () => { const mockOnLongPress = jest.fn(); render( @@ -121,12 +121,12 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnLongPress).not.toHaveBeenCalled(); }); - test('calls onLongPress for a duration greater than default onLongPressDelay', () => { + test('calls onLongPress for a duration greater than default onLongPressDelay', async () => { const mockOnLongPress = jest.fn(); render( @@ -134,12 +134,12 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); expect(mockOnLongPress).toHaveBeenCalled(); }); - test('calls onLongPress when duration is greater than specified onLongPressDelay', () => { + test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); @@ -152,18 +152,20 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); - userEvent.press(screen.getByText('press me'), { pressDuration: 1000 }); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 1000, + }); expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); }); - test('does not calls onPress when onLongPress is called', () => { + test('does not calls onPress when onLongPress is called', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); @@ -172,13 +174,13 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); }); - test('works on TouchableOpacity', () => { + test('works on TouchableOpacity', async () => { const mockOnPress = jest.fn(); render( @@ -186,12 +188,12 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnPress).toHaveBeenCalled(); }); - test('works on TouchableHighlight', () => { + test('works on TouchableHighlight', async () => { const mockOnPress = jest.fn(); render( @@ -199,12 +201,12 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnPress).toHaveBeenCalled(); }); - test('works on Text', () => { + test('works on Text', async () => { const mockOnPress = jest.fn(); const mockOnPressIn = jest.fn(); const mockOnPressOut = jest.fn(); @@ -218,14 +220,14 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnPress).toHaveBeenCalled(); expect(mockOnPressIn).toHaveBeenCalled(); expect(mockOnPressOut).toHaveBeenCalled(); }); - test('doesnt trigger on disabled Text', () => { + test('doesnt trigger on disabled Text', async () => { const mockOnPress = jest.fn(); const mockOnPressIn = jest.fn(); const mockOnPressOut = jest.fn(); @@ -240,14 +242,14 @@ describe('userEvent.press', () => { press me ); - userEvent.press(screen.getByText('press me')); + await userEvent.press(screen.getByText('press me')); expect(mockOnPress).not.toHaveBeenCalled(); expect(mockOnPressIn).not.toHaveBeenCalled(); expect(mockOnPressOut).not.toHaveBeenCalled(); }); - test('works on TetInput', () => { + test('works on TetInput', async () => { const mockOnPressIn = jest.fn(); const mockOnPressOut = jest.fn(); @@ -258,13 +260,13 @@ describe('userEvent.press', () => { onPressOut={mockOnPressOut} /> ); - userEvent.press(screen.getByPlaceholderText('email')); + await userEvent.press(screen.getByPlaceholderText('email')); expect(mockOnPressIn).toHaveBeenCalled(); expect(mockOnPressOut).toHaveBeenCalled(); }); - test('does not call onPressIn and onPressOut on non editable TetInput', () => { + test('does not call onPressIn and onPressOut on non editable TetInput', async () => { const mockOnPressIn = jest.fn(); const mockOnPressOut = jest.fn(); @@ -276,7 +278,7 @@ describe('userEvent.press', () => { onPressOut={mockOnPressOut} /> ); - userEvent.press(screen.getByPlaceholderText('email')); + await userEvent.press(screen.getByPlaceholderText('email')); expect(mockOnPressIn).not.toHaveBeenCalled(); expect(mockOnPressOut).not.toHaveBeenCalled(); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index c364df238..52b471f3a 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -17,10 +17,10 @@ export type PressOptions = { pressDuration: number; }; -export const press = ( +export const press = async ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } -) => { +): Promise => { // Text and TextInput components are mocked in React Native preset so the mock // doesn't implement the pressability class // Thus we need to call the props directly on the host component From b772e04dab19c790b3be5a7e2f26767bd9cbfc49 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 4 May 2023 12:07:24 +0200 Subject: [PATCH 13/44] feat: add support for real timers for userEvent.press --- .../userEvent.press.real-timers.test.tsx | 294 ++++++++++++++++++ .../userEvent/userEvent.press.test.tsx | 18 +- src/userEvent/constants.ts | 4 + src/userEvent/touchEvents.ts | 31 +- 4 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/userEvent/userEvent.press.real-timers.test.tsx create mode 100644 src/userEvent/constants.ts diff --git a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx new file mode 100644 index 000000000..54df3cdd9 --- /dev/null +++ b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx @@ -0,0 +1,294 @@ +import React from 'react'; +import { + Pressable, + Text, + TextInput, + TouchableHighlight, + TouchableOpacity, + View, +} from 'react-native'; +import { render, screen, userEvent } from '../../pure'; + +describe('userEvent.press using real timers', () => { + beforeEach(() => { + jest.useRealTimers(); + }); + + test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { + const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + ); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).toHaveBeenCalled(); + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); + }); + + test('does not call press when pressable is disabled', async () => { + const mockOnPress = jest.fn(); + + render(); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not call press when pointer events is none', async () => { + const mockOnPress = jest.fn(); + + render( + + ); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not call press when pointer events is box-none', async () => { + const mockOnPress = jest.fn(); + + render( + + ); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not call press when parent has pointer events box-only', async () => { + const mockOnPress = jest.fn(); + + render( + + + + ); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('calls press when pressable has pointer events box-only', async () => { + const mockOnPress = jest.fn(); + + render( + + ); + + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('crawls up in the tree to find an element that responds to touch events', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('does not call onLongPress for pressDuration of 0', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + }); + + test('calls onLongPress for a duration greater than default onLongPressDelay', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); + + test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + + await userEvent.press(screen.getByText('press me'), { + pressDuration: 1000, + }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + }); + + test('does not calls onPress when onLongPress is called', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('works on TouchableOpacity', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on TouchableHighlight', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on Text', async () => { + const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); + }); + + test('doesnt trigger on disabled Text', async () => { + const mockOnPress = jest.fn(); + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).not.toHaveBeenCalled(); + expect(mockOnPressIn).not.toHaveBeenCalled(); + expect(mockOnPressOut).not.toHaveBeenCalled(); + }); + + test('works on TetInput', async () => { + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + ); + await userEvent.press(screen.getByPlaceholderText('email')); + + expect(mockOnPressIn).toHaveBeenCalled(); + expect(mockOnPressOut).toHaveBeenCalled(); + }); + + test('does not call onPressIn and onPressOut on non editable TetInput', async () => { + const mockOnPressIn = jest.fn(); + const mockOnPressOut = jest.fn(); + + render( + + ); + await userEvent.press(screen.getByPlaceholderText('email')); + + expect(mockOnPressIn).not.toHaveBeenCalled(); + expect(mockOnPressOut).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 203d1dfb0..847af3102 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -9,9 +9,11 @@ import { } from 'react-native'; import { render, screen, userEvent } from '../../pure'; -jest.useFakeTimers(); +describe('userEvent.press with fake timers', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); -describe('userEvent.press', () => { test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { const mockOnPress = jest.fn(); const mockOnPressIn = jest.fn(); @@ -134,7 +136,9 @@ describe('userEvent.press', () => { press me ); - await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); expect(mockOnLongPress).toHaveBeenCalled(); }); @@ -152,7 +156,9 @@ describe('userEvent.press', () => { press me ); - await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); @@ -174,7 +180,9 @@ describe('userEvent.press', () => { press me ); - await userEvent.press(screen.getByText('press me'), { pressDuration: 500 }); + await userEvent.press(screen.getByText('press me'), { + pressDuration: 500, + }); expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); diff --git a/src/userEvent/constants.ts b/src/userEvent/constants.ts new file mode 100644 index 000000000..9042deddb --- /dev/null +++ b/src/userEvent/constants.ts @@ -0,0 +1,4 @@ +// These are constants defined in the React Native repo + +// Used to define the delay before calling onPressOut after a press +export const DEFAULT_MIN_PRESS_DURATION = 130; diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 52b471f3a..07d8a734a 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -4,6 +4,8 @@ import { getHostParent } from '../helpers/component-tree'; import { filterNodeByType } from '../helpers/filterNodeByType'; import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; import { getHostComponentNames } from '../helpers/host-component-names'; +import { jestFakeTimersAreEnabled } from '../helpers/timers'; +import { DEFAULT_MIN_PRESS_DURATION } from './constants'; const defaultPressEvent = { persist: jest.fn(), @@ -45,7 +47,7 @@ export const press = async ( } if (isEnabledTouchResponder(element)) { - triggerPressEvent(element, options); + await triggerPressEvent(element, options); return; } @@ -54,24 +56,37 @@ export const press = async ( return; } - press(hostParentElement, options); + await press(hostParentElement, options); }; -const triggerPressEvent = ( +const triggerPressEvent = async ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } ) => { - act(() => { + const areFakeTimersEnabled = jestFakeTimersAreEnabled(); + + await act(async () => { element.props.onResponderGrant({ ...defaultPressEvent, dispatchConfig: { registrationName: 'onResponderGrant' }, }); - jest.advanceTimersByTime(options.pressDuration); + + if (areFakeTimersEnabled) { + jest.advanceTimersByTime(options.pressDuration); + } else { + await wait(options.pressDuration); + } + element.props.onResponderRelease({ ...defaultPressEvent, dispatchConfig: { registrationName: 'onResponderRelease' }, }); - jest.runOnlyPendingTimers(); + + if (areFakeTimersEnabled) { + jest.runOnlyPendingTimers(); + } else { + await wait(DEFAULT_MIN_PRESS_DURATION); + } }); }; @@ -82,3 +97,7 @@ const isEnabledTouchResponder = (element: ReactTestInstance) => { element.props.onStartShouldSetResponder() ); }; + +const wait = async (durationInMs: number) => { + await new Promise((resolve) => setTimeout(resolve, durationInMs)); +}; From e3a5352a260d375918d3ac868c7607acf0deafb2 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 4 May 2023 12:27:06 +0200 Subject: [PATCH 14/44] refactor: create a longPress api and remove duration option from press api --- .../userEvent.longPress.real-timers.test.tsx | 63 +++++++++++++++++++ .../userEvent/userEvent.longPress.test.tsx | 63 +++++++++++++++++++ .../userEvent.press.real-timers.test.tsx | 60 ------------------ .../userEvent/userEvent.press.test.tsx | 60 ------------------ src/userEvent/constants.ts | 3 + src/userEvent/touchEvents.ts | 12 +++- src/userEvent/userEvent.ts | 3 +- 7 files changed, 141 insertions(+), 123 deletions(-) create mode 100644 src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx create mode 100644 src/__tests__/userEvent/userEvent.longPress.test.tsx diff --git a/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx b/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx new file mode 100644 index 000000000..68e799248 --- /dev/null +++ b/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Pressable, Text } from 'react-native'; +import { render, screen, userEvent } from '../../pure'; + +describe('userEvent.longPress with fake timers', () => { + beforeEach(() => { + jest.useRealTimers(); + }); + + test('calls onLongPress if the delayLongPress is the default one', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); + + test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + + await userEvent.longPress(screen.getByText('press me'), { + pressDuration: 1000, + }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + }); + + test('does not calls onPress when onLongPress is called', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/userEvent/userEvent.longPress.test.tsx b/src/__tests__/userEvent/userEvent.longPress.test.tsx new file mode 100644 index 000000000..cf6f227e2 --- /dev/null +++ b/src/__tests__/userEvent/userEvent.longPress.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Pressable, Text } from 'react-native'; +import { render, screen, userEvent } from '../../pure'; + +describe('userEvent.longPress with fake timers', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + test('calls onLongPress if the delayLongPress is the default one', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); + + test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + + await userEvent.longPress(screen.getByText('press me'), { + pressDuration: 1000, + }); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).toHaveBeenCalledTimes(1); + }); + + test('does not calls onPress when onLongPress is called', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx index 54df3cdd9..6d6db1355 100644 --- a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx @@ -128,66 +128,6 @@ describe('userEvent.press using real timers', () => { expect(mockOnLongPress).not.toHaveBeenCalled(); }); - test('calls onLongPress for a duration greater than default onLongPressDelay', async () => { - const mockOnLongPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - }); - - test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { - const mockOnLongPress = jest.fn(); - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).not.toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); - - await userEvent.press(screen.getByText('press me'), { - pressDuration: 1000, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); - }); - - test('does not calls onPress when onLongPress is called', async () => { - const mockOnLongPress = jest.fn(); - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - expect(mockOnPress).not.toHaveBeenCalled(); - }); - test('works on TouchableOpacity', async () => { const mockOnPress = jest.fn(); diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx index 847af3102..6978846bf 100644 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.test.tsx @@ -128,66 +128,6 @@ describe('userEvent.press with fake timers', () => { expect(mockOnLongPress).not.toHaveBeenCalled(); }); - test('calls onLongPress for a duration greater than default onLongPressDelay', async () => { - const mockOnLongPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - }); - - test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { - const mockOnLongPress = jest.fn(); - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).not.toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); - - await userEvent.press(screen.getByText('press me'), { - pressDuration: 1000, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); - }); - - test('does not calls onPress when onLongPress is called', async () => { - const mockOnLongPress = jest.fn(); - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me'), { - pressDuration: 500, - }); - - expect(mockOnLongPress).toHaveBeenCalled(); - expect(mockOnPress).not.toHaveBeenCalled(); - }); - test('works on TouchableOpacity', async () => { const mockOnPress = jest.fn(); diff --git a/src/userEvent/constants.ts b/src/userEvent/constants.ts index 9042deddb..8d237a0db 100644 --- a/src/userEvent/constants.ts +++ b/src/userEvent/constants.ts @@ -2,3 +2,6 @@ // Used to define the delay before calling onPressOut after a press export const DEFAULT_MIN_PRESS_DURATION = 130; + +// Default minimum press duration to trigger a long press +export const DEFAULT_LONG_PRESS_DELAY_MS = 500; diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 07d8a734a..19439a795 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -19,7 +19,7 @@ export type PressOptions = { pressDuration: number; }; -export const press = async ( +export const basePress = async ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } ): Promise => { @@ -56,9 +56,17 @@ export const press = async ( return; } - await press(hostParentElement, options); + await basePress(hostParentElement, options); }; +export const press = async (element: ReactTestInstance): Promise => + basePress(element); + +export const longPress = async ( + element: ReactTestInstance, + options: PressOptions = { pressDuration: 500 } +): Promise => basePress(element, options); + const triggerPressEvent = async ( element: ReactTestInstance, options: PressOptions = { pressDuration: 0 } diff --git a/src/userEvent/userEvent.ts b/src/userEvent/userEvent.ts index 84e47b46d..da9ff6929 100644 --- a/src/userEvent/userEvent.ts +++ b/src/userEvent/userEvent.ts @@ -1,5 +1,6 @@ -import { press } from './touchEvents'; +import { press, longPress } from './touchEvents'; export const userEvent = { press, + longPress, }; From 010656e7998b47c641240cfa3cc2fe775ad22baf Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 4 May 2023 12:35:25 +0200 Subject: [PATCH 15/44] feat: add warning for users when userEvent is used with real timers --- .../userEvent.longPress.real-timers.test.tsx | 13 +++++++++++++ .../userEvent/userEvent.press.real-timers.test.tsx | 13 +++++++++++++ src/userEvent/touchEvents.ts | 4 ++++ src/userEvent/utils/warnAboutRealTimers.ts | 6 ++++++ 4 files changed, 36 insertions(+) create mode 100644 src/userEvent/utils/warnAboutRealTimers.ts diff --git a/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx b/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx index 68e799248..52acb16ff 100644 --- a/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx +++ b/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx @@ -1,6 +1,11 @@ import React from 'react'; import { Pressable, Text } from 'react-native'; import { render, screen, userEvent } from '../../pure'; +import * as WarnAboutRealTimers from '../../userEvent/utils/warnAboutRealTimers'; + +const mockWarnAboutRealTimers = jest + .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') + .mockImplementation(); describe('userEvent.longPress with fake timers', () => { beforeEach(() => { @@ -60,4 +65,12 @@ describe('userEvent.longPress with fake timers', () => { expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); }); + + test('warns about using real timers with userEvent', async () => { + render(); + + await userEvent.longPress(screen.getByTestId('pressable')); + + expect(mockWarnAboutRealTimers).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx index 6d6db1355..ea3a78ab0 100644 --- a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx +++ b/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx @@ -8,6 +8,11 @@ import { View, } from 'react-native'; import { render, screen, userEvent } from '../../pure'; +import * as WarnAboutRealTimers from '../../userEvent/utils/warnAboutRealTimers'; + +const mockWarnAboutRealTimers = jest + .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') + .mockImplementation(); describe('userEvent.press using real timers', () => { beforeEach(() => { @@ -231,4 +236,12 @@ describe('userEvent.press using real timers', () => { expect(mockOnPressIn).not.toHaveBeenCalled(); expect(mockOnPressOut).not.toHaveBeenCalled(); }); + + test('warns about using real timers with userEvent', async () => { + render(); + + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockWarnAboutRealTimers).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts index 19439a795..89dc2db6d 100644 --- a/src/userEvent/touchEvents.ts +++ b/src/userEvent/touchEvents.ts @@ -6,6 +6,7 @@ import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointe import { getHostComponentNames } from '../helpers/host-component-names'; import { jestFakeTimersAreEnabled } from '../helpers/timers'; import { DEFAULT_MIN_PRESS_DURATION } from './constants'; +import { warnAboutRealTimers } from './utils/warnAboutRealTimers'; const defaultPressEvent = { persist: jest.fn(), @@ -72,6 +73,9 @@ const triggerPressEvent = async ( options: PressOptions = { pressDuration: 0 } ) => { const areFakeTimersEnabled = jestFakeTimersAreEnabled(); + if (!areFakeTimersEnabled) { + warnAboutRealTimers(); + } await act(async () => { element.props.onResponderGrant({ diff --git a/src/userEvent/utils/warnAboutRealTimers.ts b/src/userEvent/utils/warnAboutRealTimers.ts new file mode 100644 index 000000000..8d69a4f01 --- /dev/null +++ b/src/userEvent/utils/warnAboutRealTimers.ts @@ -0,0 +1,6 @@ +export const warnAboutRealTimers = () => { + // eslint-disable-next-line no-console + console.warn(`It is not recommended to use userEvent without using fake timers +Some events involve duration so your tests may take a long time to run. +For instance calling userEvent.longPress with real timers will take 500ms`); +}; From 1785009878d6283bf42d0ac8ee37462dd97dd7a1 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:30:43 +0200 Subject: [PATCH 16/44] refactor: rewrite press using common user event code --- .../userEvent/userEvent.press.test.tsx | 234 ----------------- src/pure.ts | 2 - src/test-utils/events.ts | 4 + src/user-event/event-builder/common.ts | 10 + src/user-event/index.ts | 3 + .../__snapshots__/press.test.tsx.snap | 54 ---- .../__tests__/longPress.real-timers.test.tsx} | 32 ++- .../press/__tests__/longPress.test.tsx} | 28 +- .../__tests__/press.real-timers.test.tsx} | 161 +++++++----- src/user-event/press/__tests__/press.test.tsx | 248 ++++++++++++++++-- .../press}/constants.ts | 0 src/user-event/press/index.ts | 2 +- src/user-event/press/press.ts | 109 +++++++- .../press}/utils/warnAboutRealTimers.ts | 0 src/user-event/setup/setup.ts | 8 +- src/user-event/utils/wait.ts | 4 +- src/userEvent/touchEvents.ts | 115 -------- src/userEvent/userEvent.ts | 6 - 18 files changed, 488 insertions(+), 532 deletions(-) delete mode 100644 src/__tests__/userEvent/userEvent.press.test.tsx delete mode 100644 src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap rename src/{__tests__/userEvent/userEvent.longPress.real-timers.test.tsx => user-event/press/__tests__/longPress.real-timers.test.tsx} (69%) rename src/{__tests__/userEvent/userEvent.longPress.test.tsx => user-event/press/__tests__/longPress.test.tsx} (68%) rename src/{__tests__/userEvent/userEvent.press.real-timers.test.tsx => user-event/press/__tests__/press.real-timers.test.tsx} (51%) rename src/{userEvent => user-event/press}/constants.ts (100%) rename src/{userEvent => user-event/press}/utils/warnAboutRealTimers.ts (100%) delete mode 100644 src/userEvent/touchEvents.ts delete mode 100644 src/userEvent/userEvent.ts diff --git a/src/__tests__/userEvent/userEvent.press.test.tsx b/src/__tests__/userEvent/userEvent.press.test.tsx deleted file mode 100644 index 6978846bf..000000000 --- a/src/__tests__/userEvent/userEvent.press.test.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React from 'react'; -import { - Pressable, - Text, - TextInput, - TouchableHighlight, - TouchableOpacity, - View, -} from 'react-native'; -import { render, screen, userEvent } from '../../pure'; - -describe('userEvent.press with fake timers', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); - - render( - - ); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).toHaveBeenCalled(); - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); - }); - - test('does not call press when pressable is disabled', async () => { - const mockOnPress = jest.fn(); - - render(); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).not.toHaveBeenCalled(); - }); - - test('does not call press when pointer events is none', async () => { - const mockOnPress = jest.fn(); - - render( - - ); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).not.toHaveBeenCalled(); - }); - - test('does not call press when pointer events is box-none', async () => { - const mockOnPress = jest.fn(); - - render( - - ); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).not.toHaveBeenCalled(); - }); - - test('does not call press when parent has pointer events box-only', async () => { - const mockOnPress = jest.fn(); - - render( - - - - ); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).not.toHaveBeenCalled(); - }); - - test('calls press when pressable has pointer events box-only', async () => { - const mockOnPress = jest.fn(); - - render( - - ); - - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).toHaveBeenCalled(); - }); - - test('crawls up in the tree to find an element that responds to touch events', async () => { - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnPress).toHaveBeenCalled(); - }); - - test('does not call onLongPress for pressDuration of 0', async () => { - const mockOnLongPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnLongPress).not.toHaveBeenCalled(); - }); - - test('works on TouchableOpacity', async () => { - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnPress).toHaveBeenCalled(); - }); - - test('works on TouchableHighlight', async () => { - const mockOnPress = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnPress).toHaveBeenCalled(); - }); - - test('works on Text', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnPress).toHaveBeenCalled(); - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); - }); - - test('doesnt trigger on disabled Text', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); - - render( - - press me - - ); - await userEvent.press(screen.getByText('press me')); - - expect(mockOnPress).not.toHaveBeenCalled(); - expect(mockOnPressIn).not.toHaveBeenCalled(); - expect(mockOnPressOut).not.toHaveBeenCalled(); - }); - - test('works on TetInput', async () => { - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); - - render( - - ); - await userEvent.press(screen.getByPlaceholderText('email')); - - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); - }); - - test('does not call onPressIn and onPressOut on non editable TetInput', async () => { - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); - - render( - - ); - await userEvent.press(screen.getByPlaceholderText('email')); - - expect(mockOnPressIn).not.toHaveBeenCalled(); - expect(mockOnPressOut).not.toHaveBeenCalled(); - }); -}); diff --git a/src/pure.ts b/src/pure.ts index 305296779..2996c6cd1 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -14,7 +14,6 @@ export { export { getDefaultNormalizer } from './matches'; export { renderHook } from './renderHook'; export { screen } from './screen'; -export { userEvent } from './userEvent/userEvent'; export type { RenderOptions, @@ -23,4 +22,3 @@ export type { } from './render'; export type { RenderHookOptions, RenderHookResult } from './renderHook'; export type { Config } from './config'; -export type { PressOptions } from './userEvent/touchEvents'; diff --git a/src/test-utils/events.ts b/src/test-utils/events.ts index b6a0dd13c..c34095346 100644 --- a/src/test-utils/events.ts +++ b/src/test-utils/events.ts @@ -18,3 +18,7 @@ export function createEventLogger() { return { events, logEvent }; } + +export function getEventsName(events: EventEntry[]) { + return events.map((event) => event.name); +} diff --git a/src/user-event/event-builder/common.ts b/src/user-event/event-builder/common.ts index 1e732eb3f..0bb4a6527 100644 --- a/src/user-event/event-builder/common.ts +++ b/src/user-event/event-builder/common.ts @@ -45,4 +45,14 @@ export const CommonEventBuilder = { }, }; }, + + press: () => { + return { + persist: jest.fn(), + nativeEvent: { + timestamp: new Date().getTime(), + }, + currentTarget: { measure: jest.fn() }, + }; + }, }; diff --git a/src/user-event/index.ts b/src/user-event/index.ts index c90f608bd..fb6fa8d37 100644 --- a/src/user-event/index.ts +++ b/src/user-event/index.ts @@ -1,11 +1,14 @@ import { ReactTestInstance } from 'react-test-renderer'; import { setup } from './setup'; +import { PressOptions } from './press/press'; export const userEvent = { setup, // Direct access for User Event v13 compatibility press: (element: ReactTestInstance) => setup().press(element), + longPress: (element: ReactTestInstance, options?: PressOptions) => + setup().longPress(element, options), type: (element: ReactTestInstance, text: string) => setup().type(element, text), }; diff --git a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap deleted file mode 100644 index 87a9c5931..000000000 --- a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`user.press() dispatches required events on Text 1`] = ` -[ - { - "name": "pressIn", - "payload": { - "nativeEvent": { - "changedTouches": [], - "identifier": 0, - "locationX": 0, - "locationY": 0, - "pageX": 0, - "pageY": 0, - "target": 0, - "timestamp": 0, - "touches": [], - }, - }, - }, - { - "name": "press", - "payload": { - "nativeEvent": { - "changedTouches": [], - "identifier": 0, - "locationX": 0, - "locationY": 0, - "pageX": 0, - "pageY": 0, - "target": 0, - "timestamp": 0, - "touches": [], - }, - }, - }, - { - "name": "pressOut", - "payload": { - "nativeEvent": { - "changedTouches": [], - "identifier": 0, - "locationX": 0, - "locationY": 0, - "pageX": 0, - "pageY": 0, - "target": 0, - "timestamp": 0, - "touches": [], - }, - }, - }, -] -`; diff --git a/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx similarity index 69% rename from src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx rename to src/user-event/press/__tests__/longPress.real-timers.test.tsx index 52acb16ff..7a6fa6ea0 100644 --- a/src/__tests__/userEvent/userEvent.longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -1,26 +1,28 @@ import React from 'react'; import { Pressable, Text } from 'react-native'; -import { render, screen, userEvent } from '../../pure'; -import * as WarnAboutRealTimers from '../../userEvent/utils/warnAboutRealTimers'; +import { render, screen } from '../../../pure'; +import { userEvent } from '../..'; +import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers'; const mockWarnAboutRealTimers = jest .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') .mockImplementation(); -describe('userEvent.longPress with fake timers', () => { +describe('userEvent.longPress with real timers', () => { beforeEach(() => { jest.useRealTimers(); }); test('calls onLongPress if the delayLongPress is the default one', async () => { const mockOnLongPress = jest.fn(); + const user = userEvent.setup(); render( press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).toHaveBeenCalled(); }); @@ -28,6 +30,7 @@ describe('userEvent.longPress with fake timers', () => { test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); + const user = userEvent.setup(); render( { press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); - await userEvent.longPress(screen.getByText('press me'), { + await user.longPress(screen.getByText('press me'), { pressDuration: 1000, }); @@ -54,18 +57,33 @@ describe('userEvent.longPress with fake timers', () => { test('does not calls onPress when onLongPress is called', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); + const user = userEvent.setup(); render( press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); }); + test('longPress is accessible directly in userEvent', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); + test('warns about using real timers with userEvent', async () => { render(); diff --git a/src/__tests__/userEvent/userEvent.longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx similarity index 68% rename from src/__tests__/userEvent/userEvent.longPress.test.tsx rename to src/user-event/press/__tests__/longPress.test.tsx index cf6f227e2..d715b5aa7 100644 --- a/src/__tests__/userEvent/userEvent.longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Pressable, Text } from 'react-native'; -import { render, screen, userEvent } from '../../pure'; +import { render, screen } from '../../../pure'; +import { userEvent } from '../..'; describe('userEvent.longPress with fake timers', () => { beforeEach(() => { @@ -9,13 +10,14 @@ describe('userEvent.longPress with fake timers', () => { test('calls onLongPress if the delayLongPress is the default one', async () => { const mockOnLongPress = jest.fn(); + const user = userEvent.setup(); render( press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).toHaveBeenCalled(); }); @@ -23,6 +25,7 @@ describe('userEvent.longPress with fake timers', () => { test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); + const user = userEvent.setup(); render( { press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); - await userEvent.longPress(screen.getByText('press me'), { + await user.longPress(screen.getByText('press me'), { pressDuration: 1000, }); @@ -49,15 +52,30 @@ describe('userEvent.longPress with fake timers', () => { test('does not calls onPress when onLongPress is called', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); + const user = userEvent.setup(); render( press me ); - await userEvent.longPress(screen.getByText('press me')); + await user.longPress(screen.getByText('press me')); expect(mockOnLongPress).toHaveBeenCalled(); expect(mockOnPress).not.toHaveBeenCalled(); }); + + test('longPress is accessible directly in userEvent', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + + await userEvent.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).toHaveBeenCalled(); + }); }); diff --git a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx similarity index 51% rename from src/__tests__/userEvent/userEvent.press.real-timers.test.tsx rename to src/user-event/press/__tests__/press.real-timers.test.tsx index ea3a78ab0..ca88c5a32 100644 --- a/src/__tests__/userEvent/userEvent.press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Pressable, Text, @@ -7,104 +7,126 @@ import { TouchableOpacity, View, } from 'react-native'; -import { render, screen, userEvent } from '../../pure'; -import * as WarnAboutRealTimers from '../../userEvent/utils/warnAboutRealTimers'; +import { createEventLogger, getEventsName } from '../../../test-utils'; +import { render, screen } from '../../..'; +import { userEvent } from '../..'; +import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers'; const mockWarnAboutRealTimers = jest .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') .mockImplementation(); -describe('userEvent.press using real timers', () => { +describe('userEvent.press with real timers', () => { beforeEach(() => { jest.useRealTimers(); }); test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); render( ); - await userEvent.press(screen.getByTestId('pressable')); + await user.press(screen.getByTestId('pressable')); - expect(mockOnPress).toHaveBeenCalled(); - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); }); - test('does not call press when pressable is disabled', async () => { - const mockOnPress = jest.fn(); + test('does not trigger event when pressable is disabled', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); - render(); - await userEvent.press(screen.getByTestId('pressable')); + render( + + ); + await user.press(screen.getByTestId('pressable')); - expect(mockOnPress).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); test('does not call press when pointer events is none', async () => { - const mockOnPress = jest.fn(); + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); render( ); - await userEvent.press(screen.getByTestId('pressable')); + await user.press(screen.getByTestId('pressable')); - expect(mockOnPress).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); test('does not call press when pointer events is box-none', async () => { - const mockOnPress = jest.fn(); + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); render( ); - await userEvent.press(screen.getByTestId('pressable')); + await user.press(screen.getByTestId('pressable')); - expect(mockOnPress).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); test('does not call press when parent has pointer events box-only', async () => { - const mockOnPress = jest.fn(); + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); render( - + ); - await userEvent.press(screen.getByTestId('pressable')); + await user.press(screen.getByTestId('pressable')); - expect(mockOnPress).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); test('calls press when pressable has pointer events box-only', async () => { - const mockOnPress = jest.fn(); + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); render( ); + await user.press(screen.getByTestId('pressable')); - await userEvent.press(screen.getByTestId('pressable')); - - expect(mockOnPress).toHaveBeenCalled(); + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); }); test('crawls up in the tree to find an element that responds to touch events', async () => { @@ -160,36 +182,30 @@ describe('userEvent.press using real timers', () => { }); test('works on Text', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); + const { events, logEvent } = createEventLogger(); render( press me ); await userEvent.press(screen.getByText('press me')); - expect(mockOnPress).toHaveBeenCalled(); - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); }); test('doesnt trigger on disabled Text', async () => { - const mockOnPress = jest.fn(); - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); + const { events, logEvent } = createEventLogger(); render( press me @@ -197,44 +213,51 @@ describe('userEvent.press using real timers', () => { ); await userEvent.press(screen.getByText('press me')); - expect(mockOnPress).not.toHaveBeenCalled(); - expect(mockOnPressIn).not.toHaveBeenCalled(); - expect(mockOnPressOut).not.toHaveBeenCalled(); + expect(events).toEqual([]); }); test('works on TetInput', async () => { - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); + const { events, logEvent } = createEventLogger(); render( ); await userEvent.press(screen.getByPlaceholderText('email')); - expect(mockOnPressIn).toHaveBeenCalled(); - expect(mockOnPressOut).toHaveBeenCalled(); + expect(getEventsName(events)).toEqual(['pressIn', 'pressOut']); }); test('does not call onPressIn and onPressOut on non editable TetInput', async () => { - const mockOnPressIn = jest.fn(); - const mockOnPressOut = jest.fn(); + const { events, logEvent } = createEventLogger(); render( ); await userEvent.press(screen.getByPlaceholderText('email')); + expect(events).toEqual([]); + }); - expect(mockOnPressIn).not.toHaveBeenCalled(); - expect(mockOnPressOut).not.toHaveBeenCalled(); + test('press is accessible directly in userEvent', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); }); test('warns about using real timers with userEvent', async () => { diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index c7dda9f2a..91e79b448 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -1,67 +1,257 @@ import * as React from 'react'; -import { Text } from 'react-native'; -import { createEventLogger } from '../../../test-utils'; -import { render } from '../../..'; +import { + Pressable, + Text, + TextInput, + TouchableHighlight, + TouchableOpacity, + View, +} from 'react-native'; +import { createEventLogger, getEventsName } from '../../../test-utils'; +import { render, screen } from '../../..'; import { userEvent } from '../..'; -beforeEach(() => { - jest.resetAllMocks(); -}); +describe('userEvent.press with fake timers', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); -describe('user.press()', () => { - it('dispatches required events on Text', async () => { + test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { const { events, logEvent } = createEventLogger(); const user = userEvent.setup(); - const screen = render( - ); + await user.press(screen.getByTestId('pressable')); + + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); + }); - await user.press(screen.getByTestId('view')); + test('does not trigger event when pressable is disabled', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + + ); + await user.press(screen.getByTestId('pressable')); - const eventNames = events.map((event) => event.name); - expect(eventNames).toEqual(['pressIn', 'press', 'pressOut']); - expect(events).toMatchSnapshot(); + expect(events).toEqual([]); }); - it('supports direct access', async () => { + test('does not call press when pointer events is none', async () => { const { events, logEvent } = createEventLogger(); - const screen = render( - ); + await user.press(screen.getByTestId('pressable')); + + expect(events).toEqual([]); + }); + + test('does not call press when pointer events is box-none', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); - await userEvent.press(screen.getByTestId('view')); + render( + + ); + await user.press(screen.getByTestId('pressable')); - const eventNames = events.map((event) => event.name); - expect(eventNames).toEqual(['pressIn', 'press', 'pressOut']); + expect(events).toEqual([]); }); - it.each(['modern', 'legacy'])('works with fake %s timers', async (type) => { - jest.useFakeTimers({ legacyFakeTimers: type === 'legacy' }); + test('does not call press when parent has pointer events box-only', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + + + + ); + await user.press(screen.getByTestId('pressable')); + + expect(events).toEqual([]); + }); + test('calls press when pressable has pointer events box-only', async () => { const { events, logEvent } = createEventLogger(); const user = userEvent.setup(); - const screen = render( + + render( + + ); + await user.press(screen.getByTestId('pressable')); + + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); + }); + + test('crawls up in the tree to find an element that responds to touch events', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('does not call onLongPress for pressDuration of 0', async () => { + const mockOnLongPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); + }); + + test('works on TouchableOpacity', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on TouchableHighlight', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on Text', async () => { + const { events, logEvent } = createEventLogger(); + + render( + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); + }); + + test('doesnt trigger on disabled Text', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + press me + + ); + await userEvent.press(screen.getByText('press me')); + + expect(events).toEqual([]); + }); + + test('works on TetInput', async () => { + const { events, logEvent } = createEventLogger(); + + render( + ); + await userEvent.press(screen.getByPlaceholderText('email')); + + expect(getEventsName(events)).toEqual(['pressIn', 'pressOut']); + }); + + test('does not call onPressIn and onPressOut on non editable TetInput', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + ); + await userEvent.press(screen.getByPlaceholderText('email')); + expect(events).toEqual([]); + }); + + test('press is accessible directly in userEvent', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + + ); - await user.press(screen.getByTestId('view')); + await userEvent.press(screen.getByText('press me')); - const eventNames = events.map((event) => event.name); - expect(eventNames).toEqual(['pressIn', 'press', 'pressOut']); + expect(mockOnPress).toHaveBeenCalled(); }); }); diff --git a/src/userEvent/constants.ts b/src/user-event/press/constants.ts similarity index 100% rename from src/userEvent/constants.ts rename to src/user-event/press/constants.ts diff --git a/src/user-event/press/index.ts b/src/user-event/press/index.ts index dffd68f90..c7acfc260 100644 --- a/src/user-event/press/index.ts +++ b/src/user-event/press/index.ts @@ -1 +1 @@ -export { press } from './press'; +export { press, longPress } from './press'; diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index be8178315..a0e6a7629 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,16 +1,111 @@ import { ReactTestInstance } from 'react-test-renderer'; import { EventBuilder } from '../event-builder'; import { UserEventInstance } from '../setup'; -import { dispatchHostEvent, wait } from '../utils'; +import { wait } from '../utils'; +import act from '../../act'; +import { getHostParent } from '../../helpers/component-tree'; +import { filterNodeByType } from '../../helpers/filterNodeByType'; +import { isHostElementPointerEventEnabled } from '../../helpers/isHostElementPointerEventEnabled'; +import { getHostComponentNames } from '../../helpers/host-component-names'; +import { jestFakeTimersAreEnabled } from '../../helpers/timers'; +import { DEFAULT_MIN_PRESS_DURATION } from './constants'; +import { warnAboutRealTimers } from './utils/warnAboutRealTimers'; + +export type PressOptions = { + pressDuration: number; +}; + +const basePress = async ( + config: UserEventInstance['config'], + element: ReactTestInstance, + options: PressOptions = { pressDuration: 0 } +): Promise => { + // Text and TextInput components are mocked in React Native preset so the mock + // doesn't implement the pressability class + // Thus we need to call the props directly on the host component + const isEnabledHostText = + filterNodeByType(element, getHostComponentNames().text) && + !element.props.disabled; + const isEnabledTextInput = + filterNodeByType(element, getHostComponentNames().textInput) && + element.props.editable !== false; + + if (isEnabledHostText || isEnabledTextInput) { + const { onPressIn, onPress, onPressOut } = element.props; + if (onPressIn) { + onPressIn(EventBuilder.Common.press()); + } + if (onPress) { + onPress(EventBuilder.Common.press()); + } + if (onPressOut) { + onPressOut(EventBuilder.Common.press()); + } + } + + if (isEnabledTouchResponder(element)) { + await triggerPressEvent(config, element, options); + return; + } + + const hostParentElement = getHostParent(element); + if (!hostParentElement) { + return; + } + + await basePress(config, hostParentElement, options); +}; export async function press( this: UserEventInstance, element: ReactTestInstance -) { - // TODO provide real implementation - dispatchHostEvent(element, 'pressIn', EventBuilder.Common.touch()); +): Promise { + await basePress(this.config, element); +} - await wait(this.config); - dispatchHostEvent(element, 'press', EventBuilder.Common.touch()); - dispatchHostEvent(element, 'pressOut', EventBuilder.Common.touch()); +export async function longPress( + this: UserEventInstance, + element: ReactTestInstance, + options: PressOptions = { pressDuration: 500 } +): Promise { + await basePress(this.config, element, options); } + +const triggerPressEvent = async ( + config: UserEventInstance['config'], + element: ReactTestInstance, + options: PressOptions = { pressDuration: 0 } +) => { + const areFakeTimersEnabled = jestFakeTimersAreEnabled(); + if (!areFakeTimersEnabled) { + warnAboutRealTimers(); + } + + await act(async () => { + element.props.onResponderGrant({ + ...EventBuilder.Common.press(), + dispatchConfig: { registrationName: 'onResponderGrant' }, + }); + + await wait(config, options.pressDuration); + + element.props.onResponderRelease({ + ...EventBuilder.Common.press(), + dispatchConfig: { registrationName: 'onResponderRelease' }, + }); + + if (areFakeTimersEnabled) { + jest.runOnlyPendingTimers(); + } else { + await wait(config, DEFAULT_MIN_PRESS_DURATION); + } + }); +}; + +const isEnabledTouchResponder = (element: ReactTestInstance) => { + return ( + isHostElementPointerEventEnabled(element) && + element.props.onStartShouldSetResponder && + element.props.onStartShouldSetResponder() + ); +}; diff --git a/src/userEvent/utils/warnAboutRealTimers.ts b/src/user-event/press/utils/warnAboutRealTimers.ts similarity index 100% rename from src/userEvent/utils/warnAboutRealTimers.ts rename to src/user-event/press/utils/warnAboutRealTimers.ts diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index cb504f928..c1adfd214 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -1,7 +1,8 @@ import { ReactTestInstance } from 'react-test-renderer'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; -import { press } from '../press'; +import { press, longPress } from '../press'; import { type } from '../type'; +import { PressOptions } from '../press/press'; export interface UserEventSetupOptions { /** @@ -68,6 +69,10 @@ function createConfig(options?: UserEventSetupOptions): UserEventConfig { export interface UserEventInstance { config: UserEventConfig; press: (element: ReactTestInstance) => Promise; + longPress: ( + element: ReactTestInstance, + options?: PressOptions + ) => Promise; type: (element: ReactTestInstance, text: string) => Promise; } @@ -79,6 +84,7 @@ function createInstance(config: UserEventConfig): UserEventInstance { // We need to bind these functions, as they access the config through 'this.config'. const api = { press: press.bind(instance), + longPress: longPress.bind(instance), type: type.bind(instance), }; diff --git a/src/user-event/utils/wait.ts b/src/user-event/utils/wait.ts index 7b355cd36..4c34e6a8b 100644 --- a/src/user-event/utils/wait.ts +++ b/src/user-event/utils/wait.ts @@ -1,7 +1,7 @@ import { UserEventConfig } from '../setup'; -export function wait(config: UserEventConfig) { - const delay = config.delay; +export function wait(config: UserEventConfig, durationInMs?: number) { + const delay = durationInMs ?? config.delay; if (typeof delay !== 'number') { return; } diff --git a/src/userEvent/touchEvents.ts b/src/userEvent/touchEvents.ts deleted file mode 100644 index 89dc2db6d..000000000 --- a/src/userEvent/touchEvents.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ReactTestInstance } from 'react-test-renderer'; -import act from '../act'; -import { getHostParent } from '../helpers/component-tree'; -import { filterNodeByType } from '../helpers/filterNodeByType'; -import { isHostElementPointerEventEnabled } from '../helpers/isHostElementPointerEventEnabled'; -import { getHostComponentNames } from '../helpers/host-component-names'; -import { jestFakeTimersAreEnabled } from '../helpers/timers'; -import { DEFAULT_MIN_PRESS_DURATION } from './constants'; -import { warnAboutRealTimers } from './utils/warnAboutRealTimers'; - -const defaultPressEvent = { - persist: jest.fn(), - nativeEvent: { - timestamp: new Date().getTime(), - }, - currentTarget: { measure: jest.fn() }, -}; - -export type PressOptions = { - pressDuration: number; -}; - -export const basePress = async ( - element: ReactTestInstance, - options: PressOptions = { pressDuration: 0 } -): Promise => { - // Text and TextInput components are mocked in React Native preset so the mock - // doesn't implement the pressability class - // Thus we need to call the props directly on the host component - const isEnabledHostText = - filterNodeByType(element, getHostComponentNames().text) && - !element.props.disabled; - const isEnabledTextInput = - filterNodeByType(element, getHostComponentNames().textInput) && - element.props.editable !== false; - - if (isEnabledHostText || isEnabledTextInput) { - const { onPressIn, onPress, onPressOut } = element.props; - if (onPressIn) { - onPressIn(defaultPressEvent); - } - if (onPress) { - onPress(defaultPressEvent); - } - if (onPressOut) { - onPressOut(defaultPressEvent); - } - } - - if (isEnabledTouchResponder(element)) { - await triggerPressEvent(element, options); - return; - } - - const hostParentElement = getHostParent(element); - if (!hostParentElement) { - return; - } - - await basePress(hostParentElement, options); -}; - -export const press = async (element: ReactTestInstance): Promise => - basePress(element); - -export const longPress = async ( - element: ReactTestInstance, - options: PressOptions = { pressDuration: 500 } -): Promise => basePress(element, options); - -const triggerPressEvent = async ( - element: ReactTestInstance, - options: PressOptions = { pressDuration: 0 } -) => { - const areFakeTimersEnabled = jestFakeTimersAreEnabled(); - if (!areFakeTimersEnabled) { - warnAboutRealTimers(); - } - - await act(async () => { - element.props.onResponderGrant({ - ...defaultPressEvent, - dispatchConfig: { registrationName: 'onResponderGrant' }, - }); - - if (areFakeTimersEnabled) { - jest.advanceTimersByTime(options.pressDuration); - } else { - await wait(options.pressDuration); - } - - element.props.onResponderRelease({ - ...defaultPressEvent, - dispatchConfig: { registrationName: 'onResponderRelease' }, - }); - - if (areFakeTimersEnabled) { - jest.runOnlyPendingTimers(); - } else { - await wait(DEFAULT_MIN_PRESS_DURATION); - } - }); -}; - -const isEnabledTouchResponder = (element: ReactTestInstance) => { - return ( - isHostElementPointerEventEnabled(element) && - element.props.onStartShouldSetResponder && - element.props.onStartShouldSetResponder() - ); -}; - -const wait = async (durationInMs: number) => { - await new Promise((resolve) => setTimeout(resolve, durationInMs)); -}; diff --git a/src/userEvent/userEvent.ts b/src/userEvent/userEvent.ts deleted file mode 100644 index da9ff6929..000000000 --- a/src/userEvent/userEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { press, longPress } from './touchEvents'; - -export const userEvent = { - press, - longPress, -}; From 71cade31bdba40c04e19938e66af4989bc8b0701 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:45:27 +0200 Subject: [PATCH 17/44] refactor: remove duplicate pointerEventEnabled method --- src/fireEvent.ts | 24 ++----------------- ...entEnabled.ts => isPointerEventEnabled.ts} | 4 ++-- src/user-event/press/press.ts | 4 ++-- 3 files changed, 6 insertions(+), 26 deletions(-) rename src/helpers/{isHostElementPointerEventEnabled.ts => isPointerEventEnabled.ts} (89%) diff --git a/src/fireEvent.ts b/src/fireEvent.ts index a00dd7ac9..35122fb37 100644 --- a/src/fireEvent.ts +++ b/src/fireEvent.ts @@ -1,7 +1,8 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from './act'; -import { getHostParent, isHostElement } from './helpers/component-tree'; +import { isHostElement } from './helpers/component-tree'; import { getHostComponentNames } from './helpers/host-component-names'; +import { isPointerEventEnabled } from './helpers/isPointerEventEnabled'; type EventHandler = (...args: unknown[]) => unknown; @@ -19,27 +20,6 @@ export function isTouchResponder(element: ReactTestInstance) { ); } -export function isPointerEventEnabled( - element: ReactTestInstance, - isParent?: boolean -): boolean { - const pointerEvents = element.props.pointerEvents; - if (pointerEvents === 'none') { - return false; - } - - if (isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none') { - return false; - } - - const parent = getHostParent(element); - if (!parent) { - return true; - } - - return isPointerEventEnabled(parent, true); -} - /** * List of events affected by `pointerEvents` prop. * diff --git a/src/helpers/isHostElementPointerEventEnabled.ts b/src/helpers/isPointerEventEnabled.ts similarity index 89% rename from src/helpers/isHostElementPointerEventEnabled.ts rename to src/helpers/isPointerEventEnabled.ts index 6fe378c54..a864c99fe 100644 --- a/src/helpers/isHostElementPointerEventEnabled.ts +++ b/src/helpers/isPointerEventEnabled.ts @@ -7,7 +7,7 @@ import { getHostParent } from './component-tree'; // 'box-none': The View is never the target of touch events but its subviews can be // 'box-only': The view can be the target of touch events but its subviews cannot be // see the official react native doc https://reactnative.dev/docs/view#pointerevents -export const isHostElementPointerEventEnabled = ( +export const isPointerEventEnabled = ( element: ReactTestInstance, isParent?: boolean ): boolean => { @@ -22,5 +22,5 @@ export const isHostElementPointerEventEnabled = ( const hostParent = getHostParent(element); if (!hostParent) return true; - return isHostElementPointerEventEnabled(hostParent, true); + return isPointerEventEnabled(hostParent, true); }; diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index a0e6a7629..31210413f 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -5,7 +5,7 @@ import { wait } from '../utils'; import act from '../../act'; import { getHostParent } from '../../helpers/component-tree'; import { filterNodeByType } from '../../helpers/filterNodeByType'; -import { isHostElementPointerEventEnabled } from '../../helpers/isHostElementPointerEventEnabled'; +import { isPointerEventEnabled } from '../../helpers/isPointerEventEnabled'; import { getHostComponentNames } from '../../helpers/host-component-names'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; import { DEFAULT_MIN_PRESS_DURATION } from './constants'; @@ -104,7 +104,7 @@ const triggerPressEvent = async ( const isEnabledTouchResponder = (element: ReactTestInstance) => { return ( - isHostElementPointerEventEnabled(element) && + isPointerEventEnabled(element) && element.props.onStartShouldSetResponder && element.props.onStartShouldSetResponder() ); From ed3ece068258d252e731e2150f1be0d198ddacdf Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:48:17 +0200 Subject: [PATCH 18/44] refactor: remove check on fake timers in user.press --- src/user-event/press/press.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 31210413f..494b70212 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -94,11 +94,7 @@ const triggerPressEvent = async ( dispatchConfig: { registrationName: 'onResponderRelease' }, }); - if (areFakeTimersEnabled) { - jest.runOnlyPendingTimers(); - } else { - await wait(config, DEFAULT_MIN_PRESS_DURATION); - } + await wait(config, DEFAULT_MIN_PRESS_DURATION); }); }; From f6c52f0d0109bcc6477000e55d5eecb1e6bf70db Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:50:34 +0200 Subject: [PATCH 19/44] refactor: change order of functions in press file to have exports first --- src/user-event/press/press.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 494b70212..7b626d4c9 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -15,6 +15,21 @@ export type PressOptions = { pressDuration: number; }; +export async function press( + this: UserEventInstance, + element: ReactTestInstance +): Promise { + await basePress(this.config, element); +} + +export async function longPress( + this: UserEventInstance, + element: ReactTestInstance, + options: PressOptions = { pressDuration: 500 } +): Promise { + await basePress(this.config, element, options); +} + const basePress = async ( config: UserEventInstance['config'], element: ReactTestInstance, @@ -56,21 +71,6 @@ const basePress = async ( await basePress(config, hostParentElement, options); }; -export async function press( - this: UserEventInstance, - element: ReactTestInstance -): Promise { - await basePress(this.config, element); -} - -export async function longPress( - this: UserEventInstance, - element: ReactTestInstance, - options: PressOptions = { pressDuration: 500 } -): Promise { - await basePress(this.config, element, options); -} - const triggerPressEvent = async ( config: UserEventInstance['config'], element: ReactTestInstance, From 8aa8bb13b0f0ae9443a74f4ec844d26aa2fc6efc Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:58:25 +0200 Subject: [PATCH 20/44] feat: add delay before press --- src/user-event/press/press.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 7b626d4c9..855df0fe7 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -47,6 +47,7 @@ const basePress = async ( if (isEnabledHostText || isEnabledTextInput) { const { onPressIn, onPress, onPressOut } = element.props; + await wait(config); if (onPressIn) { onPressIn(EventBuilder.Common.press()); } @@ -81,6 +82,8 @@ const triggerPressEvent = async ( warnAboutRealTimers(); } + await wait(config); + await act(async () => { element.props.onResponderGrant({ ...EventBuilder.Common.press(), From 1a1f4f3c824d2d168dcfd3e6d1ae8e1debebc472 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 18:59:06 +0200 Subject: [PATCH 21/44] feat: wait min press duration before calling onPressout for text and textInput --- src/user-event/press/press.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 855df0fe7..1ca5bac09 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -55,6 +55,7 @@ const basePress = async ( onPress(EventBuilder.Common.press()); } if (onPressOut) { + await wait(config, DEFAULT_MIN_PRESS_DURATION); onPressOut(EventBuilder.Common.press()); } } From cddd4f7456db125e70d1a036b39b3a56b7f5c047 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 14 May 2023 23:09:23 +0200 Subject: [PATCH 22/44] chore: improve coverage --- .../press/utils/warnAboutRealTimers.test.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/user-event/press/utils/warnAboutRealTimers.test.tsx diff --git a/src/user-event/press/utils/warnAboutRealTimers.test.tsx b/src/user-event/press/utils/warnAboutRealTimers.test.tsx new file mode 100644 index 000000000..4319ef95c --- /dev/null +++ b/src/user-event/press/utils/warnAboutRealTimers.test.tsx @@ -0,0 +1,13 @@ +import { warnAboutRealTimers } from './warnAboutRealTimers'; + +const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + +test('logs a warning about usign real timers with user event', () => { + warnAboutRealTimers(); + + expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` + "It is not recommended to use userEvent without using fake timers + Some events involve duration so your tests may take a long time to run. + For instance calling userEvent.longPress with real timers will take 500ms" + `); +}); From 0a549dc9201b63d098d3e4802e624ee5311ae55e Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 18 May 2023 21:01:22 +0200 Subject: [PATCH 23/44] feat: account for press duration when waiting for press out --- src/user-event/press/press.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 1ca5bac09..778724375 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -98,7 +98,9 @@ const triggerPressEvent = async ( dispatchConfig: { registrationName: 'onResponderRelease' }, }); - await wait(config, DEFAULT_MIN_PRESS_DURATION); + if (DEFAULT_MIN_PRESS_DURATION - options.pressDuration > 0) { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.pressDuration); + } }); }; From dc7452284f352f2b366011610467d7f79d3365fa Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 18 May 2023 21:31:07 +0200 Subject: [PATCH 24/44] fix: wait for press duration also when pressing text or textinput --- src/user-event/press/press.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 778724375..5bb5e6ca8 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -54,8 +54,11 @@ const basePress = async ( if (onPress) { onPress(EventBuilder.Common.press()); } + await wait(config, options.pressDuration); if (onPressOut) { - await wait(config, DEFAULT_MIN_PRESS_DURATION); + if (DEFAULT_MIN_PRESS_DURATION - options.pressDuration > 0) { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.pressDuration); + } onPressOut(EventBuilder.Common.press()); } } From 4a5c1832b9b7da8015176a2f357b754bf4ef75f9 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 25 May 2023 12:42:33 +0200 Subject: [PATCH 25/44] docs: add documentation on press and longpress --- website/docs/UserEvent.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md index 3ccdacab1..88dae612b 100644 --- a/website/docs/UserEvent.md +++ b/website/docs/UserEvent.md @@ -28,3 +28,39 @@ Creates User Event instances which can be used to trigger events. ### Options - `delay` - controls the default delay between subsequent events, e.g. keystrokes, etc. - `advanceTimers` - time advancement utility function that should be used for fake timers. The default setup handles both real and Jest fake timers. + + +## `press()` + +```ts +type( + element: ReactTestInstance, +): Promise +``` + +Example +```ts +const user = userEvent.setup(); + +await user.press(touchable); +``` + +This helper simulates a press on any pressable element, e.g. Pressable, Text or TextInput. Unlike fireEvent.press which is a simpler API that will only call the onPress prop, this simumlates the entire press event in a more realistic way by reproducing what really happens when a user presses a pressable component. This means for instance for onPressIn and onPressOut props will also be invoked. + +## `longPress()` + +```ts +type( + element: ReactTestInstance, + options: { pressDuration: number } = { pressDuration: 500 } +): Promise +``` + +Example +```ts +const user = userEvent.setup(); + +await user.longPress(touchable); +``` + +Simulates a press of long duration. In React Native the `onLongPress` prop is called if the press duration is at least 500ms which is the default duration for this helper. Other than the press duration this will behave exactly as `press`. The duration is customizable through the options. This should be useful if you use the `delayLongPress` prop. When using real timers this will take 500ms so it is highly recommended to use that API with fake timers to prevent test taking a long time to run. \ No newline at end of file From e1b456d8253e6a66ecfba73bbadca15abe1c6016 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Thu, 25 May 2023 13:03:00 +0200 Subject: [PATCH 26/44] fix: check pointer events for Text and TextInput --- .../__tests__/press.real-timers.test.tsx | 34 +++++++++++++++++++ src/user-event/press/__tests__/press.test.tsx | 34 +++++++++++++++++++ src/user-event/press/press.ts | 2 ++ 3 files changed, 70 insertions(+) diff --git a/src/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx index ca88c5a32..66a3f127d 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -216,6 +216,25 @@ describe('userEvent.press with real timers', () => { expect(events).toEqual([]); }); + test('doesnt trigger on Text with disabled pointer events', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + + press me + + + ); + await userEvent.press(screen.getByText('press me')); + + expect(events).toEqual([]); + }); + test('works on TetInput', async () => { const { events, logEvent } = createEventLogger(); @@ -246,6 +265,21 @@ describe('userEvent.press with real timers', () => { expect(events).toEqual([]); }); + test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + ); + await userEvent.press(screen.getByPlaceholderText('email')); + expect(events).toEqual([]); + }); + test('press is accessible directly in userEvent', async () => { const mockOnPress = jest.fn(); diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index 91e79b448..0ab895606 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -211,6 +211,25 @@ describe('userEvent.press with fake timers', () => { expect(events).toEqual([]); }); + test('doesnt trigger on Text with disabled pointer events', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + + press me + + + ); + await userEvent.press(screen.getByText('press me')); + + expect(events).toEqual([]); + }); + test('works on TetInput', async () => { const { events, logEvent } = createEventLogger(); @@ -241,6 +260,21 @@ describe('userEvent.press with fake timers', () => { expect(events).toEqual([]); }); + test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + ); + await userEvent.press(screen.getByPlaceholderText('email')); + expect(events).toEqual([]); + }); + test('press is accessible directly in userEvent', async () => { const mockOnPress = jest.fn(); diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 5bb5e6ca8..ff94faad6 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -40,9 +40,11 @@ const basePress = async ( // Thus we need to call the props directly on the host component const isEnabledHostText = filterNodeByType(element, getHostComponentNames().text) && + isPointerEventEnabled(element) && !element.props.disabled; const isEnabledTextInput = filterNodeByType(element, getHostComponentNames().textInput) && + isPointerEventEnabled(element) && element.props.editable !== false; if (isEnabledHostText || isEnabledTextInput) { From 233db485a570cf241c85cc1b8fce8c61082822b7 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 09:32:45 +0200 Subject: [PATCH 27/44] chore: fixes and tweaks on userEvent docs --- website/docs/UserEvent.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md index 88dae612b..4628dc992 100644 --- a/website/docs/UserEvent.md +++ b/website/docs/UserEvent.md @@ -33,7 +33,7 @@ Creates User Event instances which can be used to trigger events. ## `press()` ```ts -type( +press( element: ReactTestInstance, ): Promise ``` @@ -42,7 +42,7 @@ Example ```ts const user = userEvent.setup(); -await user.press(touchable); +await user.press(element); ``` This helper simulates a press on any pressable element, e.g. Pressable, Text or TextInput. Unlike fireEvent.press which is a simpler API that will only call the onPress prop, this simumlates the entire press event in a more realistic way by reproducing what really happens when a user presses a pressable component. This means for instance for onPressIn and onPressOut props will also be invoked. @@ -50,7 +50,7 @@ This helper simulates a press on any pressable element, e.g. Pressable, Text or ## `longPress()` ```ts -type( +longPress( element: ReactTestInstance, options: { pressDuration: number } = { pressDuration: 500 } ): Promise @@ -60,7 +60,7 @@ Example ```ts const user = userEvent.setup(); -await user.longPress(touchable); +await user.longPress(element); ``` Simulates a press of long duration. In React Native the `onLongPress` prop is called if the press duration is at least 500ms which is the default duration for this helper. Other than the press duration this will behave exactly as `press`. The duration is customizable through the options. This should be useful if you use the `delayLongPress` prop. When using real timers this will take 500ms so it is highly recommended to use that API with fake timers to prevent test taking a long time to run. \ No newline at end of file From 3bba09a3ae13c1e454b5ef31926e84626f2b4017 Mon Sep 17 00:00:00 2001 From: Pierre Zimmermann <64224599+pierrezimmermannbam@users.noreply.github.com> Date: Wed, 31 May 2023 09:33:47 +0200 Subject: [PATCH 28/44] Update press doc based on review suggestion Co-authored-by: Maciej Jastrzebski --- website/docs/UserEvent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md index 4628dc992..6f65a5544 100644 --- a/website/docs/UserEvent.md +++ b/website/docs/UserEvent.md @@ -45,7 +45,7 @@ const user = userEvent.setup(); await user.press(element); ``` -This helper simulates a press on any pressable element, e.g. Pressable, Text or TextInput. Unlike fireEvent.press which is a simpler API that will only call the onPress prop, this simumlates the entire press event in a more realistic way by reproducing what really happens when a user presses a pressable component. This means for instance for onPressIn and onPressOut props will also be invoked. +This helper simulates a press on any pressable element, e.g. `Pressable`, `TouchableOpacity`, `Text`, `TextInput`, etc. Unlike `fireEvent.press()` which is a simpler API that will only call the `onPress` prop, this simulates the entire press event in a more realistic way by reproducing what really happens when a user presses an interface view. This will trigger additional events like `pressIn` and `pressOut`. ## `longPress()` From c02d7813c5ba5dd6514a19f155fb8263ae5df4cd Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 09:37:26 +0200 Subject: [PATCH 29/44] refactor: rename pressDuration option to duration --- .../__tests__/longPress.real-timers.test.tsx | 2 +- .../press/__tests__/longPress.test.tsx | 2 +- .../__tests__/press.real-timers.test.tsx | 2 +- src/user-event/press/__tests__/press.test.tsx | 2 +- src/user-event/press/press.ts | 20 +++++++++---------- website/docs/UserEvent.md | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx index 7a6fa6ea0..36c044358 100644 --- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -47,7 +47,7 @@ describe('userEvent.longPress with real timers', () => { expect(mockOnPress).toHaveBeenCalledTimes(1); await user.longPress(screen.getByText('press me'), { - pressDuration: 1000, + duration: 1000, }); expect(mockOnLongPress).toHaveBeenCalled(); diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index d715b5aa7..d88e03178 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -42,7 +42,7 @@ describe('userEvent.longPress with fake timers', () => { expect(mockOnPress).toHaveBeenCalledTimes(1); await user.longPress(screen.getByText('press me'), { - pressDuration: 1000, + duration: 1000, }); expect(mockOnLongPress).toHaveBeenCalled(); diff --git a/src/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx index 66a3f127d..63d65d30b 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -142,7 +142,7 @@ describe('userEvent.press with real timers', () => { expect(mockOnPress).toHaveBeenCalled(); }); - test('does not call onLongPress for pressDuration of 0', async () => { + test('does not call onLongPress', async () => { const mockOnLongPress = jest.fn(); render( diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index 0ab895606..845ee0e20 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -137,7 +137,7 @@ describe('userEvent.press with fake timers', () => { expect(mockOnPress).toHaveBeenCalled(); }); - test('does not call onLongPress for pressDuration of 0', async () => { + test('does not call onLongPress', async () => { const mockOnLongPress = jest.fn(); render( diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index ff94faad6..169a34a49 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -12,7 +12,7 @@ import { DEFAULT_MIN_PRESS_DURATION } from './constants'; import { warnAboutRealTimers } from './utils/warnAboutRealTimers'; export type PressOptions = { - pressDuration: number; + duration: number; }; export async function press( @@ -25,7 +25,7 @@ export async function press( export async function longPress( this: UserEventInstance, element: ReactTestInstance, - options: PressOptions = { pressDuration: 500 } + options: PressOptions = { duration: 500 } ): Promise { await basePress(this.config, element, options); } @@ -33,7 +33,7 @@ export async function longPress( const basePress = async ( config: UserEventInstance['config'], element: ReactTestInstance, - options: PressOptions = { pressDuration: 0 } + options: PressOptions = { duration: 0 } ): Promise => { // Text and TextInput components are mocked in React Native preset so the mock // doesn't implement the pressability class @@ -56,10 +56,10 @@ const basePress = async ( if (onPress) { onPress(EventBuilder.Common.press()); } - await wait(config, options.pressDuration); + await wait(config, options.duration); if (onPressOut) { - if (DEFAULT_MIN_PRESS_DURATION - options.pressDuration > 0) { - await wait(config, DEFAULT_MIN_PRESS_DURATION - options.pressDuration); + if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); } onPressOut(EventBuilder.Common.press()); } @@ -81,7 +81,7 @@ const basePress = async ( const triggerPressEvent = async ( config: UserEventInstance['config'], element: ReactTestInstance, - options: PressOptions = { pressDuration: 0 } + options: PressOptions = { duration: 0 } ) => { const areFakeTimersEnabled = jestFakeTimersAreEnabled(); if (!areFakeTimersEnabled) { @@ -96,15 +96,15 @@ const triggerPressEvent = async ( dispatchConfig: { registrationName: 'onResponderGrant' }, }); - await wait(config, options.pressDuration); + await wait(config, options.duration); element.props.onResponderRelease({ ...EventBuilder.Common.press(), dispatchConfig: { registrationName: 'onResponderRelease' }, }); - if (DEFAULT_MIN_PRESS_DURATION - options.pressDuration > 0) { - await wait(config, DEFAULT_MIN_PRESS_DURATION - options.pressDuration); + if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); } }); }; diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md index 6f65a5544..3a5d18075 100644 --- a/website/docs/UserEvent.md +++ b/website/docs/UserEvent.md @@ -52,7 +52,7 @@ This helper simulates a press on any pressable element, e.g. `Pressable`, `Touch ```ts longPress( element: ReactTestInstance, - options: { pressDuration: number } = { pressDuration: 500 } + options: { duration: number } = { duration: 500 } ): Promise ``` From 8801a5646aa52694ea5bd52cc8b02e8f351a9d3f Mon Sep 17 00:00:00 2001 From: Pierre Zimmermann <64224599+pierrezimmermannbam@users.noreply.github.com> Date: Wed, 31 May 2023 09:49:17 +0200 Subject: [PATCH 30/44] Update longPress doc based on review suggestion Co-authored-by: Maciej Jastrzebski --- website/docs/UserEvent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/UserEvent.md b/website/docs/UserEvent.md index 3a5d18075..908002ea2 100644 --- a/website/docs/UserEvent.md +++ b/website/docs/UserEvent.md @@ -63,4 +63,4 @@ const user = userEvent.setup(); await user.longPress(element); ``` -Simulates a press of long duration. In React Native the `onLongPress` prop is called if the press duration is at least 500ms which is the default duration for this helper. Other than the press duration this will behave exactly as `press`. The duration is customizable through the options. This should be useful if you use the `delayLongPress` prop. When using real timers this will take 500ms so it is highly recommended to use that API with fake timers to prevent test taking a long time to run. \ No newline at end of file +Simulates a long press user interaction. In React Native the `longPress` event is emitted when the press duration exceeds long press threshold (by default 500 ms). In other aspects this actions behaves similar to regular `press` action, e.g. by emitting `pressIn` and `pressOut` events. The press duration is customisable through the options. This should be useful if you use the `delayLongPress` prop. When using real timers this will take 500 ms so it is highly recommended to use that API with fake timers to prevent test taking a long time to run. \ No newline at end of file From b6b0d30023891d6088055b4b96afdf773427d6e9 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 09:52:41 +0200 Subject: [PATCH 31/44] refactor: use ts doc for isPointerEventsEnabled method --- src/helpers/isPointerEventEnabled.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/helpers/isPointerEventEnabled.ts b/src/helpers/isPointerEventEnabled.ts index a864c99fe..fbd7495d3 100644 --- a/src/helpers/isPointerEventEnabled.ts +++ b/src/helpers/isPointerEventEnabled.ts @@ -1,12 +1,13 @@ import { ReactTestInstance } from 'react-test-renderer'; import { getHostParent } from './component-tree'; -// pointerEvents controls whether the View can be the target of touch events. -// 'auto': The View can be the target of touch events. -// 'none': The View is never the target of touch events. -// 'box-none': The View is never the target of touch events but its subviews can be -// 'box-only': The view can be the target of touch events but its subviews cannot be -// see the official react native doc https://reactnative.dev/docs/view#pointerevents +/** + * pointerEvents controls whether the View can be the target of touch events. + * 'auto': The View and its children can be the target of touch events. + * 'none': The View is never the target of touch events. + * 'box-none': The View is never the target of touch events but its subviews can be + * 'box-only': The view can be the target of touch events but its subviews cannot be + * see the official react native doc https://reactnative.dev/docs/view#pointerevents */ export const isPointerEventEnabled = ( element: ReactTestInstance, isParent?: boolean From c1314479cf7bd6bbfd5263ef4599d44994319098 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 09:53:40 +0200 Subject: [PATCH 32/44] refactor: rename file isPointerEventsEnabled to pointer-events --- src/fireEvent.ts | 2 +- src/helpers/{isPointerEventEnabled.ts => pointer-events.ts} | 0 src/user-event/press/press.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/helpers/{isPointerEventEnabled.ts => pointer-events.ts} (100%) diff --git a/src/fireEvent.ts b/src/fireEvent.ts index 35122fb37..cf87ec9ff 100644 --- a/src/fireEvent.ts +++ b/src/fireEvent.ts @@ -2,7 +2,7 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from './act'; import { isHostElement } from './helpers/component-tree'; import { getHostComponentNames } from './helpers/host-component-names'; -import { isPointerEventEnabled } from './helpers/isPointerEventEnabled'; +import { isPointerEventEnabled } from './helpers/pointer-events'; type EventHandler = (...args: unknown[]) => unknown; diff --git a/src/helpers/isPointerEventEnabled.ts b/src/helpers/pointer-events.ts similarity index 100% rename from src/helpers/isPointerEventEnabled.ts rename to src/helpers/pointer-events.ts diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 169a34a49..705a238cb 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -5,7 +5,7 @@ import { wait } from '../utils'; import act from '../../act'; import { getHostParent } from '../../helpers/component-tree'; import { filterNodeByType } from '../../helpers/filterNodeByType'; -import { isPointerEventEnabled } from '../../helpers/isPointerEventEnabled'; +import { isPointerEventEnabled } from '../../helpers/pointer-events'; import { getHostComponentNames } from '../../helpers/host-component-names'; import { jestFakeTimersAreEnabled } from '../../helpers/timers'; import { DEFAULT_MIN_PRESS_DURATION } from './constants'; From 327f1f1ba84759437c4966948ffc41a6728e5b52 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 10:20:15 +0200 Subject: [PATCH 33/44] refactor: split test in longPress in two --- .../__tests__/longPress.real-timers.test.tsx | 28 +++++++++++++++---- .../press/__tests__/longPress.test.tsx | 26 +++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx index 36c044358..191a4865f 100644 --- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -27,30 +27,46 @@ describe('userEvent.longPress with real timers', () => { expect(mockOnLongPress).toHaveBeenCalled(); }); - test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { + test('calls onLongPress when duration is greater than specified longPressDelay', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); const user = userEvent.setup(); render( press me ); - await user.longPress(screen.getByText('press me')); - - expect(mockOnLongPress).not.toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); await user.longPress(screen.getByText('press me'), { duration: 1000, }); expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not calls onLongPress when duration is lesser than specified longPressDelay', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + const user = userEvent.setup(); + + render( + + press me + + ); + await user.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); }); diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index d88e03178..f4ee9034d 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -29,23 +29,39 @@ describe('userEvent.longPress with fake timers', () => { render( press me ); - await user.longPress(screen.getByText('press me')); - - expect(mockOnLongPress).not.toHaveBeenCalled(); - expect(mockOnPress).toHaveBeenCalledTimes(1); await user.longPress(screen.getByText('press me'), { duration: 1000, }); expect(mockOnLongPress).toHaveBeenCalled(); + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + test('does not calls onLongPress when duration is lesser than specified onLongPressDelay', async () => { + const mockOnLongPress = jest.fn(); + const mockOnPress = jest.fn(); + const user = userEvent.setup(); + + render( + + press me + + ); + await user.longPress(screen.getByText('press me')); + + expect(mockOnLongPress).not.toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); }); From 83989b195cdda5f007c247fbc529d951721dd4df Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Wed, 31 May 2023 14:28:37 +0200 Subject: [PATCH 34/44] refactor: use Date.now instead of new Date().getTime() --- src/user-event/event-builder/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user-event/event-builder/common.ts b/src/user-event/event-builder/common.ts index 0bb4a6527..583e1f09f 100644 --- a/src/user-event/event-builder/common.ts +++ b/src/user-event/event-builder/common.ts @@ -50,7 +50,7 @@ export const CommonEventBuilder = { return { persist: jest.fn(), nativeEvent: { - timestamp: new Date().getTime(), + timestamp: Date.now(), }, currentTarget: { measure: jest.fn() }, }; From e99bb8e7053486f7aae187cb84bec4580aa20ad3 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 11 Jun 2023 12:57:09 +0200 Subject: [PATCH 35/44] refactor: also test payload of events for press and longpress --- .../press/__tests__/longPress.test.tsx | 55 +++++++++- src/user-event/press/__tests__/press.test.tsx | 100 +++++++++++++++++- 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index f4ee9034d..e0d7d3a4a 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -2,24 +2,73 @@ import React from 'react'; import { Pressable, Text } from 'react-native'; import { render, screen } from '../../../pure'; import { userEvent } from '../..'; +import { createEventLogger } from '../../../test-utils'; describe('userEvent.longPress with fake timers', () => { beforeEach(() => { jest.useFakeTimers(); + jest.setSystemTime(0); }); test('calls onLongPress if the delayLongPress is the default one', async () => { - const mockOnLongPress = jest.fn(); + const { logEvent, events } = createEventLogger(); const user = userEvent.setup(); render( - + press me ); await user.longPress(screen.getByText('press me')); - expect(mockOnLongPress).toHaveBeenCalled(); + expect(events).toMatchInlineSnapshot(` + [ + { + "name": "longPress", + "payload": { + "currentTarget": { + "measure": [MockFunction] { + "calls": [ + [ + [Function], + ], + [ + [Function], + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + "dispatchConfig": { + "registrationName": "onResponderGrant", + }, + "nativeEvent": { + "timestamp": 500, + }, + "persist": [MockFunction] { + "calls": [ + [], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + }, + ] + `); }); test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index 845ee0e20..eb05dc74d 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -14,6 +14,7 @@ import { userEvent } from '../..'; describe('userEvent.press with fake timers', () => { beforeEach(() => { jest.useFakeTimers(); + jest.setSystemTime(0); }); test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { @@ -30,7 +31,104 @@ describe('userEvent.press with fake timers', () => { ); await user.press(screen.getByTestId('pressable')); - expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(events).toMatchInlineSnapshot(` + [ + { + "name": "pressIn", + "payload": { + "currentTarget": { + "measure": [MockFunction] { + "calls": [ + [ + [Function], + ], + [ + [Function], + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + "dispatchConfig": { + "registrationName": "onResponderGrant", + }, + "nativeEvent": { + "timestamp": 0, + }, + "persist": [MockFunction] { + "calls": [ + [], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + }, + { + "name": "press", + "payload": { + "currentTarget": { + "measure": [MockFunction], + }, + "dispatchConfig": { + "registrationName": "onResponderRelease", + }, + "nativeEvent": { + "timestamp": 0, + }, + "persist": [MockFunction] { + "calls": [ + [], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + }, + { + "name": "pressOut", + "payload": { + "currentTarget": { + "measure": [MockFunction], + }, + "dispatchConfig": { + "registrationName": "onResponderRelease", + }, + "nativeEvent": { + "timestamp": 0, + }, + "persist": [MockFunction] { + "calls": [ + [], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + }, + ] + `); }); test('does not trigger event when pressable is disabled', async () => { From 0eaa2352ac375fe15f780b7774ecb32bb106079b Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 11 Jun 2023 13:02:26 +0200 Subject: [PATCH 36/44] refactor: fix typo in some longpress test names --- src/user-event/press/__tests__/longPress.real-timers.test.tsx | 4 ++-- src/user-event/press/__tests__/longPress.test.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx index 191a4865f..cd1ab97a9 100644 --- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -27,7 +27,7 @@ describe('userEvent.longPress with real timers', () => { expect(mockOnLongPress).toHaveBeenCalled(); }); - test('calls onLongPress when duration is greater than specified longPressDelay', async () => { + test('calls onLongPress when duration is greater than specified delayLongPress', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); const user = userEvent.setup(); @@ -50,7 +50,7 @@ describe('userEvent.longPress with real timers', () => { expect(mockOnPress).not.toHaveBeenCalled(); }); - test('does not calls onLongPress when duration is lesser than specified longPressDelay', async () => { + test('does not calls onLongPress when duration is lesser than specified delayLongPress', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); const user = userEvent.setup(); diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index e0d7d3a4a..a6acef735 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -71,7 +71,7 @@ describe('userEvent.longPress with fake timers', () => { `); }); - test('calls onLongPress when duration is greater than specified onLongPressDelay', async () => { + test('calls onLongPress when duration is greater than specified delayLongPress', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); const user = userEvent.setup(); @@ -94,7 +94,7 @@ describe('userEvent.longPress with fake timers', () => { expect(mockOnPress).not.toHaveBeenCalled(); }); - test('does not calls onLongPress when duration is lesser than specified onLongPressDelay', async () => { + test('does not calls onLongPress when duration is lesser than specified delayLongPress', async () => { const mockOnLongPress = jest.fn(); const mockOnPress = jest.fn(); const user = userEvent.setup(); From b78b0c37ac912f482d44b3b152689064d750eef3 Mon Sep 17 00:00:00 2001 From: Pierre Zimmermann <64224599+pierrezimmermannbam@users.noreply.github.com> Date: Sun, 11 Jun 2023 13:04:41 +0200 Subject: [PATCH 37/44] Update src/user-event/press/utils/warnAboutRealTimers.ts Co-authored-by: Maciej Jastrzebski --- src/user-event/press/utils/warnAboutRealTimers.test.tsx | 2 +- src/user-event/press/utils/warnAboutRealTimers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/user-event/press/utils/warnAboutRealTimers.test.tsx b/src/user-event/press/utils/warnAboutRealTimers.test.tsx index 4319ef95c..0c410f7ee 100644 --- a/src/user-event/press/utils/warnAboutRealTimers.test.tsx +++ b/src/user-event/press/utils/warnAboutRealTimers.test.tsx @@ -8,6 +8,6 @@ test('logs a warning about usign real timers with user event', () => { expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` "It is not recommended to use userEvent without using fake timers Some events involve duration so your tests may take a long time to run. - For instance calling userEvent.longPress with real timers will take 500ms" + For instance calling userEvent.longPress with real timers will take 500 ms" `); }); diff --git a/src/user-event/press/utils/warnAboutRealTimers.ts b/src/user-event/press/utils/warnAboutRealTimers.ts index 8d69a4f01..0ca8dc01c 100644 --- a/src/user-event/press/utils/warnAboutRealTimers.ts +++ b/src/user-event/press/utils/warnAboutRealTimers.ts @@ -2,5 +2,5 @@ export const warnAboutRealTimers = () => { // eslint-disable-next-line no-console console.warn(`It is not recommended to use userEvent without using fake timers Some events involve duration so your tests may take a long time to run. -For instance calling userEvent.longPress with real timers will take 500ms`); +For instance calling userEvent.longPress with real timers will take 500 ms`); }; From ba7103ff6b2696f9baf6dee74af2b5ec06417800 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 11 Jun 2023 13:18:17 +0200 Subject: [PATCH 38/44] refactor: merge press and touch events --- src/user-event/event-builder/common.ts | 14 +++-------- .../press/__tests__/longPress.test.tsx | 8 +++++++ src/user-event/press/__tests__/press.test.tsx | 24 +++++++++++++++++++ src/user-event/press/press.ts | 10 ++++---- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/user-event/event-builder/common.ts b/src/user-event/event-builder/common.ts index 583e1f09f..fd2842e55 100644 --- a/src/user-event/event-builder/common.ts +++ b/src/user-event/event-builder/common.ts @@ -6,6 +6,8 @@ export const CommonEventBuilder = { */ touch: () => { return { + persist: jest.fn(), + currentTarget: { measure: jest.fn() }, nativeEvent: { changedTouches: [], identifier: 0, @@ -14,7 +16,7 @@ export const CommonEventBuilder = { pageX: 0, pageY: 0, target: 0, - timestamp: 0, + timestamp: Date.now(), touches: [], }, }; @@ -45,14 +47,4 @@ export const CommonEventBuilder = { }, }; }, - - press: () => { - return { - persist: jest.fn(), - nativeEvent: { - timestamp: Date.now(), - }, - currentTarget: { measure: jest.fn() }, - }; - }, }; diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index a6acef735..472420d29 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -52,7 +52,15 @@ describe('userEvent.longPress with fake timers', () => { "registrationName": "onResponderGrant", }, "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, "timestamp": 500, + "touches": [], }, "persist": [MockFunction] { "calls": [ diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index eb05dc74d..eccdcf2fe 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -62,7 +62,15 @@ describe('userEvent.press with fake timers', () => { "registrationName": "onResponderGrant", }, "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, "timestamp": 0, + "touches": [], }, "persist": [MockFunction] { "calls": [ @@ -87,7 +95,15 @@ describe('userEvent.press with fake timers', () => { "registrationName": "onResponderRelease", }, "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, "timestamp": 0, + "touches": [], }, "persist": [MockFunction] { "calls": [ @@ -112,7 +128,15 @@ describe('userEvent.press with fake timers', () => { "registrationName": "onResponderRelease", }, "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, "timestamp": 0, + "touches": [], }, "persist": [MockFunction] { "calls": [ diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 705a238cb..ea81ecc0d 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -51,17 +51,17 @@ const basePress = async ( const { onPressIn, onPress, onPressOut } = element.props; await wait(config); if (onPressIn) { - onPressIn(EventBuilder.Common.press()); + onPressIn(EventBuilder.Common.touch()); } if (onPress) { - onPress(EventBuilder.Common.press()); + onPress(EventBuilder.Common.touch()); } await wait(config, options.duration); if (onPressOut) { if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); } - onPressOut(EventBuilder.Common.press()); + onPressOut(EventBuilder.Common.touch()); } } @@ -92,14 +92,14 @@ const triggerPressEvent = async ( await act(async () => { element.props.onResponderGrant({ - ...EventBuilder.Common.press(), + ...EventBuilder.Common.touch(), dispatchConfig: { registrationName: 'onResponderGrant' }, }); await wait(config, options.duration); element.props.onResponderRelease({ - ...EventBuilder.Common.press(), + ...EventBuilder.Common.touch(), dispatchConfig: { registrationName: 'onResponderRelease' }, }); From a094ca14c3d3c61bfb0dd32cd972e5f9e08c7b79 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 12:19:14 +0200 Subject: [PATCH 39/44] fix: return after pressing text or textinput --- src/user-event/press/__tests__/longPress.test.tsx | 2 +- src/user-event/press/press.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index 472420d29..421d9f873 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -59,7 +59,7 @@ describe('userEvent.longPress with fake timers', () => { "pageX": 0, "pageY": 0, "target": 0, - "timestamp": 500, + "timestamp": 0, "touches": [], }, "persist": [MockFunction] { diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index ea81ecc0d..404674085 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -41,7 +41,8 @@ const basePress = async ( const isEnabledHostText = filterNodeByType(element, getHostComponentNames().text) && isPointerEventEnabled(element) && - !element.props.disabled; + !element.props.disabled && + element.props.onPress; const isEnabledTextInput = filterNodeByType(element, getHostComponentNames().textInput) && isPointerEventEnabled(element) && @@ -63,6 +64,7 @@ const basePress = async ( } onPressOut(EventBuilder.Common.touch()); } + return; } if (isEnabledTouchResponder(element)) { From c8ad5bfe0b0c134f7535102137f50075f08d4537 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 12:23:43 +0200 Subject: [PATCH 40/44] refactor: extract functions to trigger press on text/textInput --- src/user-event/press/press.ts | 67 +++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 404674085..90acf4a4f 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -38,32 +38,8 @@ const basePress = async ( // Text and TextInput components are mocked in React Native preset so the mock // doesn't implement the pressability class // Thus we need to call the props directly on the host component - const isEnabledHostText = - filterNodeByType(element, getHostComponentNames().text) && - isPointerEventEnabled(element) && - !element.props.disabled && - element.props.onPress; - const isEnabledTextInput = - filterNodeByType(element, getHostComponentNames().textInput) && - isPointerEventEnabled(element) && - element.props.editable !== false; - - if (isEnabledHostText || isEnabledTextInput) { - const { onPressIn, onPress, onPressOut } = element.props; - await wait(config); - if (onPressIn) { - onPressIn(EventBuilder.Common.touch()); - } - if (onPress) { - onPress(EventBuilder.Common.touch()); - } - await wait(config, options.duration); - if (onPressOut) { - if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { - await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); - } - onPressOut(EventBuilder.Common.touch()); - } + if (isEnabledHostText(element) || isEnabledTextInput(element)) { + await triggerMockPressEvent(config, element, options); return; } @@ -118,3 +94,42 @@ const isEnabledTouchResponder = (element: ReactTestInstance) => { element.props.onStartShouldSetResponder() ); }; + +const isEnabledHostText = (element: ReactTestInstance) => { + return ( + filterNodeByType(element, getHostComponentNames().text) && + isPointerEventEnabled(element) && + !element.props.disabled && + element.props.onPress + ); +}; + +const isEnabledTextInput = (element: ReactTestInstance) => { + return ( + filterNodeByType(element, getHostComponentNames().textInput) && + isPointerEventEnabled(element) && + element.props.editable !== false + ); +}; + +const triggerMockPressEvent = async ( + config: UserEventInstance['config'], + element: ReactTestInstance, + options: PressOptions = { duration: 0 } +) => { + const { onPressIn, onPress, onPressOut } = element.props; + await wait(config); + if (onPressIn) { + onPressIn(EventBuilder.Common.touch()); + } + if (onPress) { + onPress(EventBuilder.Common.touch()); + } + await wait(config, options.duration); + if (onPressOut) { + if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); + } + onPressOut(EventBuilder.Common.touch()); + } +}; From 9f6cb60a23aedbba4eba74c4e03c21e4793837c2 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 13:14:05 +0200 Subject: [PATCH 41/44] refactor: use optional chaining --- src/user-event/press/press.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 90acf4a4f..c69c04074 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -90,8 +90,7 @@ const triggerPressEvent = async ( const isEnabledTouchResponder = (element: ReactTestInstance) => { return ( isPointerEventEnabled(element) && - element.props.onStartShouldSetResponder && - element.props.onStartShouldSetResponder() + element.props.onStartShouldSetResponder?.() ); }; From 2ba19875c7cbf6231c47a0b7387c3eaf4c012d04 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 16:15:38 +0200 Subject: [PATCH 42/44] feat: update warning when using userEvent with real timers --- src/user-event/press/utils/warnAboutRealTimers.test.tsx | 4 ++-- src/user-event/press/utils/warnAboutRealTimers.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/user-event/press/utils/warnAboutRealTimers.test.tsx b/src/user-event/press/utils/warnAboutRealTimers.test.tsx index 0c410f7ee..f07708d12 100644 --- a/src/user-event/press/utils/warnAboutRealTimers.test.tsx +++ b/src/user-event/press/utils/warnAboutRealTimers.test.tsx @@ -6,8 +6,8 @@ test('logs a warning about usign real timers with user event', () => { warnAboutRealTimers(); expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` - "It is not recommended to use userEvent without using fake timers + "It is recommended to use userEvent with fake timers Some events involve duration so your tests may take a long time to run. - For instance calling userEvent.longPress with real timers will take 500 ms" + For instance calling userEvent.longPress with real timers will take 500 ms." `); }); diff --git a/src/user-event/press/utils/warnAboutRealTimers.ts b/src/user-event/press/utils/warnAboutRealTimers.ts index 0ca8dc01c..307afaef3 100644 --- a/src/user-event/press/utils/warnAboutRealTimers.ts +++ b/src/user-event/press/utils/warnAboutRealTimers.ts @@ -1,6 +1,6 @@ export const warnAboutRealTimers = () => { // eslint-disable-next-line no-console - console.warn(`It is not recommended to use userEvent without using fake timers + console.warn(`It is recommended to use userEvent with fake timers Some events involve duration so your tests may take a long time to run. -For instance calling userEvent.longPress with real timers will take 500 ms`); +For instance calling userEvent.longPress with real timers will take 500 ms.`); }; From d1548c5057e7b9d009c60207b483b1a176f0730a Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 16:34:53 +0200 Subject: [PATCH 43/44] refactor: check on press test that longPress is not called --- src/user-event/press/__tests__/longPress.test.tsx | 5 ++++- .../press/__tests__/press.real-timers.test.tsx | 9 +++++++++ src/user-event/press/__tests__/press.test.tsx | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index 421d9f873..c9a23e00f 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -15,7 +15,10 @@ describe('userEvent.longPress with fake timers', () => { const user = userEvent.setup(); render( - + press me ); diff --git a/src/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx index 63d65d30b..10ae13d59 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -30,6 +30,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> ); @@ -48,6 +49,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> ); @@ -65,6 +67,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="none" /> @@ -83,6 +86,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="box-none" /> @@ -102,6 +106,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> @@ -120,6 +125,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="box-only" /> @@ -189,6 +195,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} > press me @@ -206,6 +213,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} disabled > press me @@ -225,6 +233,7 @@ describe('userEvent.press with real timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} > press me diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index eccdcf2fe..defe7c615 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -26,6 +26,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> ); @@ -165,6 +166,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> ); @@ -182,6 +184,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="none" /> @@ -200,6 +203,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="box-none" /> @@ -219,6 +223,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" /> @@ -237,6 +242,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} testID="pressable" pointerEvents="box-only" /> @@ -306,6 +312,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} > press me @@ -323,6 +330,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} disabled > press me @@ -342,6 +350,7 @@ describe('userEvent.press with fake timers', () => { onPress={logEvent('press')} onPressIn={logEvent('pressIn')} onPressOut={logEvent('pressOut')} + onLongPress={logEvent('longPress')} > press me From 17a2a43736ea4cea723c7bb6aec10b8957367b72 Mon Sep 17 00:00:00 2001 From: pierrezimmermann Date: Sun, 16 Jul 2023 16:48:28 +0200 Subject: [PATCH 44/44] refactor: test directly warnings logged with real timers without mocking function --- .../__tests__/longPress.real-timers.test.tsx | 23 +++++++++++-------- .../__tests__/press.real-timers.test.tsx | 23 +++++++++++-------- .../press/utils/warnAboutRealTimers.test.tsx | 13 ----------- 3 files changed, 28 insertions(+), 31 deletions(-) delete mode 100644 src/user-event/press/utils/warnAboutRealTimers.test.tsx diff --git a/src/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx index cd1ab97a9..fec234c0c 100644 --- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -4,13 +4,11 @@ import { render, screen } from '../../../pure'; import { userEvent } from '../..'; import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers'; -const mockWarnAboutRealTimers = jest - .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') - .mockImplementation(); - describe('userEvent.longPress with real timers', () => { beforeEach(() => { jest.useRealTimers(); + jest.restoreAllMocks(); + jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation(); }); test('calls onLongPress if the delayLongPress is the default one', async () => { @@ -99,12 +97,19 @@ describe('userEvent.longPress with real timers', () => { expect(mockOnLongPress).toHaveBeenCalled(); }); +}); - test('warns about using real timers with userEvent', async () => { - render(); +test('warns about using real timers with userEvent', async () => { + jest.restoreAllMocks(); + const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); - await userEvent.longPress(screen.getByTestId('pressable')); + render(); - expect(mockWarnAboutRealTimers).toHaveBeenCalledTimes(1); - }); + await userEvent.longPress(screen.getByTestId('pressable')); + + expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` + "It is recommended to use userEvent with fake timers + Some events involve duration so your tests may take a long time to run. + For instance calling userEvent.longPress with real timers will take 500 ms." + `); }); diff --git a/src/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx index 10ae13d59..fda9bf914 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -12,13 +12,11 @@ import { render, screen } from '../../..'; import { userEvent } from '../..'; import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers'; -const mockWarnAboutRealTimers = jest - .spyOn(WarnAboutRealTimers, 'warnAboutRealTimers') - .mockImplementation(); - describe('userEvent.press with real timers', () => { beforeEach(() => { jest.useRealTimers(); + jest.restoreAllMocks(); + jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation(); }); test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { @@ -302,12 +300,19 @@ describe('userEvent.press with real timers', () => { expect(mockOnPress).toHaveBeenCalled(); }); +}); - test('warns about using real timers with userEvent', async () => { - render(); +test('warns about using real timers with userEvent', async () => { + jest.restoreAllMocks(); + const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); - await userEvent.press(screen.getByTestId('pressable')); + render(); - expect(mockWarnAboutRealTimers).toHaveBeenCalledTimes(1); - }); + await userEvent.press(screen.getByTestId('pressable')); + + expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` + "It is recommended to use userEvent with fake timers + Some events involve duration so your tests may take a long time to run. + For instance calling userEvent.longPress with real timers will take 500 ms." + `); }); diff --git a/src/user-event/press/utils/warnAboutRealTimers.test.tsx b/src/user-event/press/utils/warnAboutRealTimers.test.tsx deleted file mode 100644 index f07708d12..000000000 --- a/src/user-event/press/utils/warnAboutRealTimers.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { warnAboutRealTimers } from './warnAboutRealTimers'; - -const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); - -test('logs a warning about usign real timers with user event', () => { - warnAboutRealTimers(); - - expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(` - "It is recommended to use userEvent with fake timers - Some events involve duration so your tests may take a long time to run. - For instance calling userEvent.longPress with real timers will take 500 ms." - `); -});