From f8ffc002d113c3013aee1ef0fe61fed4e5b707ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 25 Oct 2024 22:20:41 +0200 Subject: [PATCH 01/11] chore: pressable experiment --- experiments-app/src/experiments.ts | 6 ++ experiments-app/src/screens/PressEvents.tsx | 82 +++++++++++++++++++++ experiments-app/src/utils/helpers.ts | 5 +- src/__tests__/render.test.tsx | 15 +++- 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 experiments-app/src/screens/PressEvents.tsx diff --git a/experiments-app/src/experiments.ts b/experiments-app/src/experiments.ts index fdd485148..af889cfb1 100644 --- a/experiments-app/src/experiments.ts +++ b/experiments-app/src/experiments.ts @@ -1,4 +1,5 @@ import { AccessibilityScreen } from './screens/Accessibility'; +import { PressEvents } from './screens/PressEvents'; import { TextInputEventPropagation } from './screens/TextInputEventPropagation'; import { TextInputEvents } from './screens/TextInputEvents'; import { ScrollViewEvents } from './screens/ScrollViewEvents'; @@ -13,6 +14,11 @@ export const experiments = [ title: 'Accessibility', component: AccessibilityScreen, }, + { + key: 'PressEvents', + title: 'Press Events', + component: PressEvents, + }, { key: 'TextInputEvents', title: 'TextInput Events', diff --git a/experiments-app/src/screens/PressEvents.tsx b/experiments-app/src/screens/PressEvents.tsx new file mode 100644 index 000000000..a8ba3edcc --- /dev/null +++ b/experiments-app/src/screens/PressEvents.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { + StyleSheet, + SafeAreaView, + Text, + TextInput, + View, + Pressable, + TouchableOpacity, +} from 'react-native'; +import { nativeEventLogger, logEvent } from '../utils/helpers'; + +export function PressEvents() { + const [value, setValue] = React.useState(''); + + const handleChangeText = (value: string) => { + setValue(value); + logEvent('changeText', value); + }; + + return ( + + + + + + + Text + + + + + Pressable + + + + + Pressable + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + wrapper: { + padding: 20, + backgroundColor: 'yellow', + }, + textInput: { + backgroundColor: 'white', + margin: 20, + padding: 8, + fontSize: 18, + borderWidth: 1, + borderColor: 'grey', + }, +}); diff --git a/experiments-app/src/utils/helpers.ts b/experiments-app/src/utils/helpers.ts index 5993a46c4..1366177b7 100644 --- a/experiments-app/src/utils/helpers.ts +++ b/experiments-app/src/utils/helpers.ts @@ -1,5 +1,7 @@ import { NativeSyntheticEvent } from 'react-native/types'; +let lastEventTimeStamp: number | null = null; + export function nativeEventLogger(name: string) { return (event: NativeSyntheticEvent) => { logEvent(name, event?.nativeEvent); @@ -14,5 +16,6 @@ export function customEventLogger(name: string) { export function logEvent(name: string, ...args: unknown[]) { // eslint-disable-next-line no-console - console.log(`Event: ${name}`, ...args); + console.log(`[${Date.now() - (lastEventTimeStamp ?? Date.now())}ms] Event: ${name}`, ...args); + lastEventTimeStamp = Date.now(); } diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 3127963d7..327b7189e 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import * as React from 'react'; import { Pressable, Text, TextInput, View } from 'react-native'; -import { getConfig, resetToDefaults } from '../config'; +import { configure, getConfig, resetToDefaults } from '../config'; import { fireEvent, render, RenderAPI, screen } from '..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; @@ -247,7 +247,16 @@ test('supports legacy rendering', () => { expect(screen.root).toBeDefined(); }); -test('supports concurrent rendering', () => { +// Enable concurrent rendering globally +configure({ concurrentRoot: true }); + +test('globally enable concurrent rendering', () => { + render(); + expect(screen.root).toBeOnTheScreen(); +}); + +// Enable concurrent rendering locally +test('locally enable concurrent rendering', () => { render(, { concurrentRoot: true }); - expect(screen.root).toBeDefined(); + expect(screen.root).toBeOnTheScreen(); }); From 93aae18123bb233b7545d914d4825c4756976e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 25 Oct 2024 22:24:45 +0200 Subject: [PATCH 02/11] refactor: update press implementation --- src/user-event/press/press.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index c49ea7090..6de77c402 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -118,11 +118,19 @@ async function emitTextPressEvents( await wait(config); dispatchEvent(element, 'pressIn', EventBuilder.Common.touch()); - // Emit either `press` or `longPress`. - dispatchEvent(element, options.type, EventBuilder.Common.touch()); - await wait(config, options.duration); + + // Long press events are emitted before `pressOut`. + if (options.type === 'longPress') { + dispatchEvent(element, 'longPress', EventBuilder.Common.touch()); + } + dispatchEvent(element, 'pressOut', EventBuilder.Common.touch()); + + // Regular press events are emitted after `pressOut`. + if (options.type === 'press') { + dispatchEvent(element, 'press', EventBuilder.Common.touch()); + } } /** From 0bfd25e65816dd322bc655310bc8ecfcbc2a4a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 25 Oct 2024 22:31:04 +0200 Subject: [PATCH 03/11] chore: fix Text press event order --- src/user-event/press/__tests__/press.real-timers.test.tsx | 8 ++++---- src/user-event/press/__tests__/press.test.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 1ba53c27e..4844d7d41 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -198,7 +198,7 @@ describe('userEvent.press with real timers', () => { ); await userEvent.press(screen.getByText('press me')); - expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); test('does not trigger on disabled Text', async () => { @@ -240,7 +240,7 @@ describe('userEvent.press with real timers', () => { expect(events).toEqual([]); }); - test('works on TetInput', async () => { + test('works on TextInput', async () => { const { events, logEvent } = createEventLogger(); render( @@ -255,7 +255,7 @@ describe('userEvent.press with real timers', () => { expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']); }); - test('does not call onPressIn and onPressOut on non editable TetInput', async () => { + test('does not call onPressIn and onPressOut on non editable TextInput', async () => { const { events, logEvent } = createEventLogger(); render( @@ -270,7 +270,7 @@ describe('userEvent.press with real timers', () => { expect(events).toEqual([]); }); - test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => { + test('does not call onPressIn and onPressOut on TextInput with pointer events disabled', async () => { const { events, logEvent } = createEventLogger(); render( diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index c4ff8be74..e830eacc6 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -199,7 +199,7 @@ describe('userEvent.press with fake timers', () => { ); await userEvent.press(screen.getByText('press me')); - expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); test('press works on Button', async () => { From e11e75f4b90c228e71fadd6fdc982036d475d119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 25 Oct 2024 22:34:18 +0200 Subject: [PATCH 04/11] chore: fix Pressable implementation --- .../__snapshots__/press.test.tsx.snap | 8 ++--- .../__tests__/press.real-timers.test.tsx | 4 +-- src/user-event/press/__tests__/press.test.tsx | 2 +- src/user-event/press/constants.ts | 7 ----- src/user-event/press/press.ts | 29 ++++++++++--------- 5 files changed, 22 insertions(+), 28 deletions(-) delete mode 100644 src/user-event/press/constants.ts diff --git a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap index ceb2803f3..068aebacd 100644 --- a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap +++ b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap @@ -33,7 +33,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu }, }, { - "name": "press", + "name": "pressOut", "payload": { "currentTarget": { "measure": [Function], @@ -52,7 +52,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu "pageX": 0, "pageY": 0, "target": 0, - "timestamp": 0, + "timestamp": 130, "touches": [], }, "persist": [Function], @@ -63,7 +63,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu }, }, { - "name": "pressOut", + "name": "press", "payload": { "currentTarget": { "measure": [Function], @@ -82,7 +82,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu "pageX": 0, "pageY": 0, "target": 0, - "timestamp": 0, + "timestamp": 130, "touches": [], }, "persist": [Function], 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 4844d7d41..930bdff0a 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -32,7 +32,7 @@ describe('userEvent.press with real timers', () => { ); await user.press(screen.getByTestId('pressable')); - expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); test('does not trigger event when pressable is disabled', async () => { @@ -128,7 +128,7 @@ describe('userEvent.press with real timers', () => { ); await user.press(screen.getByTestId('pressable')); - expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); test('crawls up in the tree to find an element that responds to touch events', async () => { diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index e830eacc6..43ce21c67 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -129,7 +129,7 @@ describe('userEvent.press with fake timers', () => { ); await user.press(screen.getByTestId('pressable')); - expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); test('crawls up in the tree to find an element that responds to touch events', async () => { diff --git a/src/user-event/press/constants.ts b/src/user-event/press/constants.ts deleted file mode 100644 index 8d237a0db..000000000 --- a/src/user-event/press/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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; - -// Default minimum press duration to trigger a long press -export const DEFAULT_LONG_PRESS_DELAY_MS = 500; diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 6de77c402..d63df7d17 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,5 +1,4 @@ import { ReactTestInstance } from 'react-test-renderer'; -import act from '../../act'; import { getHostParent } from '../../helpers/component-tree'; import { isTextInputEditable } from '../../helpers/text-input'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; @@ -7,7 +6,10 @@ import { isHostText, isHostTextInput } from '../../helpers/host-component-names' import { EventBuilder } from '../event-builder'; import { UserEventConfig, UserEventInstance } from '../setup'; import { dispatchEvent, wait } from '../utils'; -import { DEFAULT_MIN_PRESS_DURATION } from './constants'; + +// These are constants defined in the React Native repo +export const DEFAULT_MIN_PRESS_DURATION = 130; +export const DEFAULT_LONG_PRESS_DELAY_MS = 500; export interface PressOptions { duration?: number; @@ -27,7 +29,7 @@ export async function longPress( ): Promise { await basePress(this.config, element, { type: 'longPress', - duration: options?.duration ?? 500, + duration: options?.duration ?? DEFAULT_LONG_PRESS_DELAY_MS, }); } @@ -73,18 +75,14 @@ const emitPressablePressEvents = async ( dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant()); - await wait(config, options.duration); + // We apply minimum press duration here to ensure that `press` events are emitted after `pressOut`. + // Otherwise, pressables would emit them in the reverse order, which in reality happens only for + // very short presses (< 130ms) and contradicts the React Native docs. + // See: https://reactnative.dev/docs/pressable#onpress + let duration = Math.max(options.duration, DEFAULT_MIN_PRESS_DURATION); + await wait(config, duration); dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease()); - - // React Native will wait for minimal delay of DEFAULT_MIN_PRESS_DURATION - // before emitting the `pressOut` event. We need to wait here, so that - // `press()` function does not return before that. - if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { - await act(async () => { - await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); - }); - } }; const isEnabledTouchResponder = (element: ReactTestInstance) => { @@ -127,7 +125,10 @@ async function emitTextPressEvents( dispatchEvent(element, 'pressOut', EventBuilder.Common.touch()); - // Regular press events are emitted after `pressOut`. + // Regular press events are emitted after `pressOut` according to the React Native docs. + // See: https://reactnative.dev/docs/pressable#onpress + // Experimentally for very short presses (< 130ms) `press` events are actually emitted before `onPressOut`, but + // we will ignore that as in reality most pressed would be above the 130ms threshold. if (options.type === 'press') { dispatchEvent(element, 'press', EventBuilder.Common.touch()); } From 0df6b276806276f571eefee09b728cc264418e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 25 Oct 2024 22:48:36 +0200 Subject: [PATCH 05/11] chore: revert unrelated changes --- src/__tests__/render.test.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 327b7189e..23bfbbed8 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -247,16 +247,7 @@ test('supports legacy rendering', () => { expect(screen.root).toBeDefined(); }); -// Enable concurrent rendering globally -configure({ concurrentRoot: true }); - -test('globally enable concurrent rendering', () => { - render(); - expect(screen.root).toBeOnTheScreen(); -}); - -// Enable concurrent rendering locally -test('locally enable concurrent rendering', () => { +test('supports concurrent rendering', () => { render(, { concurrentRoot: true }); expect(screen.root).toBeOnTheScreen(); }); From 474f42b98a4544bfeb6e63755a3e69f3da92fca1 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 26 Oct 2024 09:39:43 +0200 Subject: [PATCH 06/11] chore: fix lint --- src/__tests__/render.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 23bfbbed8..fae79012b 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import * as React from 'react'; import { Pressable, Text, TextInput, View } from 'react-native'; -import { configure, getConfig, resetToDefaults } from '../config'; +import { getConfig, resetToDefaults } from '../config'; import { fireEvent, render, RenderAPI, screen } from '..'; const PLACEHOLDER_FRESHNESS = 'Add custom freshness'; From 064a48460b55883b4198b2e87418306e552c9df0 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 26 Oct 2024 19:32:40 +0200 Subject: [PATCH 07/11] fix: do not send events on unmounted components --- src/fire-event.ts | 6 ++++- src/helpers/component-tree.ts | 6 ++++- src/user-event/press/__tests__/press.test.tsx | 24 +++++++++++++++++++ src/user-event/utils/dispatch-event.ts | 5 ++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index 98c7745a3..0f0287f5e 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -7,7 +7,7 @@ import { ScrollViewProps, } from 'react-native'; import act from './act'; -import { isHostElement } from './helpers/component-tree'; +import { isElementMounted, isHostElement } from './helpers/component-tree'; import { isHostScrollView, isHostTextInput } from './helpers/host-component-names'; import { isPointerEventEnabled } from './helpers/pointer-events'; import { isTextInputEditable } from './helpers/text-input'; @@ -121,6 +121,10 @@ type EventName = StringWithAutocomplete< >; function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) { + if (!isElementMounted(element)) { + return; + } + setNativeStateIfNeeded(element, eventName, data[0]); const handler = findEventHandler(element, eventName); diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 8387278b5..4a4a00897 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -1,5 +1,5 @@ import { ReactTestInstance } from 'react-test-renderer'; - +import { screen } from '../screen'; /** * ReactTestInstance referring to host element. */ @@ -13,6 +13,10 @@ export function isHostElement(element?: ReactTestInstance | null): element is Ho return typeof element?.type === 'string'; } +export function isElementMounted(element: ReactTestInstance | null) { + return getUnsafeRootElement(element) === screen.UNSAFE_root; +} + /** * Returns first host ancestor for given element. * @param element The element start traversing from. diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index 43ce21c67..190643739 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -372,3 +372,27 @@ describe('userEvent.press with fake timers', () => { expect(consoleErrorSpy).not.toHaveBeenCalled(); }); }); + +function Component() { + const [mounted, setMounted] = React.useState(true); + + const onPressIn = () => { + setMounted(false); + }; + + return ( + + {mounted && ( + + Unmount + + )} + + ); +} + +test('unmounts component', async () => { + render(); + await userEvent.press(screen.getByText('Unmount')); + expect(screen.queryByText('Unmount')).not.toBeOnTheScreen(); +}); diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts index 6d284f633..d1202fd54 100644 --- a/src/user-event/utils/dispatch-event.ts +++ b/src/user-event/utils/dispatch-event.ts @@ -1,5 +1,6 @@ import { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; +import { isElementMounted } from '../../helpers/component-tree'; /** * Basic dispatch event function used by User Event module. @@ -9,6 +10,10 @@ import act from '../../act'; * @param event event payload(s) */ export function dispatchEvent(element: ReactTestInstance, eventName: string, ...event: unknown[]) { + if (!isElementMounted(element)) { + return; + } + const handler = getEventHandler(element, eventName); if (!handler) { return; From 762b2f551b442690ac3bfa6883cb1ac45ed4e2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 30 Oct 2024 19:42:08 +0100 Subject: [PATCH 08/11] refactor: modify pressable implemenatation --- src/user-event/press/press.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index d63df7d17..cf321ce13 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,4 +1,5 @@ import { ReactTestInstance } from 'react-test-renderer'; +import act from '../../act'; import { getHostParent } from '../../helpers/component-tree'; import { isTextInputEditable } from '../../helpers/text-input'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; @@ -18,7 +19,7 @@ export interface PressOptions { export async function press(this: UserEventInstance, element: ReactTestInstance): Promise { await basePress(this.config, element, { type: 'press', - duration: 0, + duration: DEFAULT_MIN_PRESS_DURATION, }); } @@ -75,14 +76,18 @@ const emitPressablePressEvents = async ( dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant()); - // We apply minimum press duration here to ensure that `press` events are emitted after `pressOut`. - // Otherwise, pressables would emit them in the reverse order, which in reality happens only for - // very short presses (< 130ms) and contradicts the React Native docs. - // See: https://reactnative.dev/docs/pressable#onpress - let duration = Math.max(options.duration, DEFAULT_MIN_PRESS_DURATION); - await wait(config, duration); + await wait(config, options.duration); dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease()); + + // React Native will wait for minimal delay of DEFAULT_MIN_PRESS_DURATION + // before emitting the `pressOut` event. We need to wait here, so that + // `press()` function does not return before that. + if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { + await act(async () => { + await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); + }); + } }; const isEnabledTouchResponder = (element: ReactTestInstance) => { From aa96951784c079d653cfad441a5ace889f640cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 30 Oct 2024 19:55:42 +0100 Subject: [PATCH 09/11] refactor: code review changes --- .../__snapshots__/longPress.test.tsx.snap | 95 +++++++++++ .../__snapshots__/press.test.tsx.snap | 2 +- .../__tests__/longPress.real-timers.test.tsx | 67 +++++++- .../press/__tests__/longPress.test.tsx | 69 +++++++- .../__tests__/press.real-timers.test.tsx | 131 ++++++++------- src/user-event/press/__tests__/press.test.tsx | 156 ++++++++---------- src/user-event/press/press.ts | 1 + 7 files changed, 369 insertions(+), 152 deletions(-) diff --git a/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap index fa31b7a34..b1c8a3fb2 100644 --- a/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap +++ b/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap @@ -34,3 +34,98 @@ exports[`userEvent.longPress with fake timers calls onLongPress if the delayLong }, ] `; + +exports[`userEvent.longPress with fake timers works on Pressable 1`] = ` +[ + { + "name": "pressIn", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "dispatchConfig": { + "registrationName": "onResponderGrant", + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 0, + "touches": [], + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "longPress", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "dispatchConfig": { + "registrationName": "onResponderGrant", + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 0, + "touches": [], + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, + { + "name": "pressOut", + "payload": { + "currentTarget": { + "measure": [Function], + }, + "dispatchConfig": { + "registrationName": "onResponderRelease", + }, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "changedTouches": [], + "identifier": 0, + "locationX": 0, + "locationY": 0, + "pageX": 0, + "pageY": 0, + "target": 0, + "timestamp": 500, + "touches": [], + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, +] +`; diff --git a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap index 068aebacd..d4dc3df36 100644 --- a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap +++ b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOut prop of touchable 1`] = ` +exports[`userEvent.press with fake timers works on Pressable 1`] = ` [ { "name": "pressIn", 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 928e5b6d3..4c693830c 100644 --- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx +++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Pressable, Text } from 'react-native'; -import { render, screen } from '../../../pure'; +import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native'; +import { createEventLogger, getEventsNames } from '../../../test-utils'; +import { render, screen } from '../../..'; import { userEvent } from '../..'; describe('userEvent.longPress with real timers', () => { @@ -9,6 +10,68 @@ describe('userEvent.longPress with real timers', () => { jest.restoreAllMocks(); }); + test('works on Pressable', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + , + ); + + await user.longPress(screen.getByTestId('pressable')); + expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']); + }); + + test('works on TouchableOpacity', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on TouchableHighlight', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on Text', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']); + }); + test('calls onLongPress if the delayLongPress is the default one', async () => { const mockOnLongPress = 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 3716446e7..bbf5d0582 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Pressable, Text } from 'react-native'; -import { render, screen } from '../../../pure'; +import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native'; +import { createEventLogger, getEventsNames } from '../../../test-utils'; +import { render, screen } from '../../..'; import { userEvent } from '../..'; -import { createEventLogger } from '../../../test-utils'; describe('userEvent.longPress with fake timers', () => { beforeEach(() => { @@ -10,6 +10,69 @@ describe('userEvent.longPress with fake timers', () => { jest.setSystemTime(0); }); + test('works on Pressable', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + , + ); + + await user.longPress(screen.getByTestId('pressable')); + expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']); + expect(events).toMatchSnapshot(); + }); + + test('works on TouchableOpacity', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on TouchableHighlight', async () => { + const mockOnPress = jest.fn(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(mockOnPress).toHaveBeenCalled(); + }); + + test('works on Text', async () => { + const { events, logEvent } = createEventLogger(); + + render( + + press me + , + ); + + await userEvent.longPress(screen.getByText('press me')); + expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']); + }); + test('calls onLongPress if the delayLongPress is the default one', async () => { const { logEvent, events } = createEventLogger(); const user = userEvent.setup(); 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 930bdff0a..042b9eded 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { + Button, Pressable, Text, TextInput, @@ -17,7 +18,7 @@ describe('userEvent.press with real timers', () => { jest.restoreAllMocks(); }); - test('calls onPressIn, onPress and onPressOut prop of touchable', async () => { + test('works on Pressable', async () => { const { events, logEvent } = createEventLogger(); const user = userEvent.setup(); @@ -30,11 +31,79 @@ describe('userEvent.press with real timers', () => { testID="pressable" />, ); + await user.press(screen.getByTestId('pressable')); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); + }); + + 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(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); }); + test('works on TextInput', async () => { + const { events, logEvent } = createEventLogger(); + + render( + , + ); + + await userEvent.press(screen.getByPlaceholderText('email')); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']); + }); + + test('works on Button', async () => { + const { events, logEvent } = createEventLogger(); + + render(