From 6528be31aa7744dcccada039f10278ac1bdd16ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 6 Feb 2025 10:00:14 +0100 Subject: [PATCH 01/11] fix: RN Gesture Handler Pressable support --- package.json | 1 + .../react-native-gesture-handler.test.tsx | 90 +++++++++++++++++++ src/helpers/host-component-names.ts | 13 ++- src/user-event/press/press.ts | 29 +++--- yarn.lock | 42 ++++++++- 5 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/__tests__/react-native-gesture-handler.test.tsx diff --git a/package.json b/package.json index 77836fb3..d833bc56 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "chalk": "^4.1.2", "jest-matcher-utils": "^29.7.0", "pretty-format": "^29.7.0", + "react-native-gesture-handler": "^2.23.1", "redent": "^3.0.0" }, "peerDependencies": { diff --git a/src/__tests__/react-native-gesture-handler.test.tsx b/src/__tests__/react-native-gesture-handler.test.tsx new file mode 100644 index 00000000..2d6b57e0 --- /dev/null +++ b/src/__tests__/react-native-gesture-handler.test.tsx @@ -0,0 +1,90 @@ +import 'react-native-gesture-handler/jestSetup.js'; +import React from 'react'; +import { View } from 'react-native'; +import { Pressable } from 'react-native-gesture-handler'; + +import { fireEvent, render, screen, userEvent } from '..'; +import { createEventLogger, getEventsNames } from '../test-utils'; + +test('fireEvent can invoke press events for RNGH Pressable', () => { + const onPress = jest.fn(); + const onPressIn = jest.fn(); + const onPressOut = jest.fn(); + const onLongPress = jest.fn(); + + render( + + + , + ); + + const pressable = screen.getByTestId('pressable'); + + fireEvent.press(pressable); + expect(onPress).toHaveBeenCalled(); + + fireEvent(pressable, 'pressIn'); + expect(onPressIn).toHaveBeenCalled(); + + fireEvent(pressable, 'pressOut'); + expect(onPressOut).toHaveBeenCalled(); + + fireEvent(pressable, 'longPress'); + expect(onLongPress).toHaveBeenCalled(); +}); + +test('userEvent can invoke press events for RNGH Pressable', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + + + , + ); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + + + + `); + + const pressable = screen.getByTestId('pressable'); + await user.press(pressable); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); +}); diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts index 45e019bc..77da9dca 100644 --- a/src/helpers/host-component-names.ts +++ b/src/helpers/host-component-names.ts @@ -9,19 +9,28 @@ const HOST_SWITCH_NAMES = ['RCTSwitch']; const HOST_SCROLL_VIEW_NAMES = ['RCTScrollView']; const HOST_MODAL_NAMES = ['Modal']; +export type HostText = HostTestInstance & { + type: 'Text'; +}; + /** * Checks if the given element is a host Text element. * @param element The element to check. */ -export function isHostText(element: ReactTestInstance): element is HostTestInstance { +export function isHostText(element: ReactTestInstance): element is HostText { return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type); } +export type HostTextInput = HostTestInstance & { + type: 'TextInput'; + editable?: boolean; +}; + /** * Checks if the given element is a host TextInput element. * @param element The element to check. */ -export function isHostTextInput(element: ReactTestInstance): element is HostTestInstance { +export function isHostTextInput(element: ReactTestInstance): element is HostTextInput { return typeof element?.type === 'string' && HOST_TEXT_INPUT_NAMES.includes(element.type); } diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 3c964ad1..f6713115 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,8 +1,9 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; -import { getHostParent } from '../../helpers/component-tree'; -import { isHostText } from '../../helpers/host-component-names'; +import { getEventHandler } from '../../event-handler'; +import { getHostParent, isHostElement } from '../../helpers/component-tree'; +import { isHostTextInput } from '../../helpers/host-component-names'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; import { isEditableTextInput } from '../../helpers/text-input'; import { EventBuilder } from '../event-builder'; @@ -45,13 +46,13 @@ const basePress = async ( element: ReactTestInstance, options: BasePressOptions, ): Promise => { - if (isPressableText(element)) { - await emitTextPressEvents(config, element, options); + if (isEditableTextInput(element) && isPointerEventEnabled(element)) { + await emitTextInputPressEvents(config, element, options); return; } - if (isEditableTextInput(element) && isPointerEventEnabled(element)) { - await emitTextInputPressEvents(config, element, options); + if (isHostPressable(element)) { + await emitTextPressEvents(config, element, options); return; } @@ -96,16 +97,16 @@ const isEnabledTouchResponder = (element: ReactTestInstance) => { return isPointerEventEnabled(element) && element.props.onStartShouldSetResponder?.(); }; -const isPressableText = (element: ReactTestInstance) => { - const hasPressEventHandler = Boolean( - element.props.onPress || - element.props.onLongPress || - element.props.onPressIn || - element.props.onPressOut, - ); +const isHostPressable = (element: ReactTestInstance) => { + const hasPressEventHandler = + getEventHandler(element, 'press') || + getEventHandler(element, 'longPress') || + getEventHandler(element, 'pressIn') || + getEventHandler(element, 'pressOut'); return ( - isHostText(element) && + isHostElement(element) && + !isHostTextInput(element) && isPointerEventEnabled(element) && !element.props.disabled && hasPressEventHandler diff --git a/yarn.lock b/yarn.lock index f74d8cdf..4b5b106b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1601,6 +1601,15 @@ __metadata: languageName: node linkType: hard +"@egjs/hammerjs@npm:^2.0.17": + version: 2.0.17 + resolution: "@egjs/hammerjs@npm:2.0.17" + dependencies: + "@types/hammerjs": "npm:^2.0.36" + checksum: 10c0/dbedc15a0e633f887c08394bd636faf6a3abd05726dc0909a0e01209d5860a752d9eca5e512da623aecfabe665f49f1d035de3103eb2f9022c5cea692f9cc9be + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.1 resolution: "@eslint-community/eslint-utils@npm:4.4.1" @@ -2844,6 +2853,7 @@ __metadata: pretty-format: "npm:^29.7.0" react: "npm:^19.0.0" react-native: "npm:0.78.0" + react-native-gesture-handler: "npm:^2.23.1" react-test-renderer: "npm:^19.0.0" redent: "npm:^3.0.0" release-it: "npm:^18.0.0" @@ -2924,6 +2934,13 @@ __metadata: languageName: node linkType: hard +"@types/hammerjs@npm:^2.0.36": + version: 2.0.46 + resolution: "@types/hammerjs@npm:2.0.46" + checksum: 10c0/f3c1cb20dc2f0523f7b8c76065078544d50d8ae9b0edc1f62fed657210ed814266ff2dfa835d2c157a075991001eec3b64c88bf92e3e6e895c0db78d05711d06 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6103,6 +6120,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.0": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: "npm:^16.7.0" + checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74 + languageName: node + linkType: hard + "hosted-git-info@npm:^7.0.0": version: 7.0.2 resolution: "hosted-git-info@npm:7.0.2" @@ -9141,7 +9167,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 @@ -9162,6 +9188,20 @@ __metadata: languageName: node linkType: hard +"react-native-gesture-handler@npm:^2.23.1": + version: 2.23.1 + resolution: "react-native-gesture-handler@npm:2.23.1" + dependencies: + "@egjs/hammerjs": "npm:^2.0.17" + hoist-non-react-statics: "npm:^3.3.0" + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/02ad664662a529836aaae2f6bce5943ccd3193d33641e6328d1306e6cb82eb5d859c5f9975c557f6e6daf1ffb7e7f6cd8432dac70a727110bff887e554673c7c + languageName: node + linkType: hard + "react-native@npm:0.78.0": version: 0.78.0 resolution: "react-native@npm:0.78.0" From cb311c695eac4d7a77560aebca377e3188f619fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 6 Feb 2025 10:54:39 +0100 Subject: [PATCH 02/11] . --- src/helpers/host-component-names.ts | 8 ++------ src/user-event/press/press.ts | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts index 77da9dca..2e334ca6 100644 --- a/src/helpers/host-component-names.ts +++ b/src/helpers/host-component-names.ts @@ -9,15 +9,11 @@ const HOST_SWITCH_NAMES = ['RCTSwitch']; const HOST_SCROLL_VIEW_NAMES = ['RCTScrollView']; const HOST_MODAL_NAMES = ['Modal']; -export type HostText = HostTestInstance & { - type: 'Text'; -}; - /** * Checks if the given element is a host Text element. * @param element The element to check. */ -export function isHostText(element: ReactTestInstance): element is HostText { +export function isHostText(element: ReactTestInstance): element is HostTestInstance { return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type); } @@ -30,7 +26,7 @@ export type HostTextInput = HostTestInstance & { * Checks if the given element is a host TextInput element. * @param element The element to check. */ -export function isHostTextInput(element: ReactTestInstance): element is HostTextInput { +export function isHostTextInput(element: ReactTestInstance): element is HostTestInstance { return typeof element?.type === 'string' && HOST_TEXT_INPUT_NAMES.includes(element.type); } diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index f6713115..5ebd0b80 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -51,7 +51,7 @@ const basePress = async ( return; } - if (isHostPressable(element)) { + if (!isHostTextInput(element) && isHostPressable(element)) { await emitTextPressEvents(config, element, options); return; } @@ -106,7 +106,6 @@ const isHostPressable = (element: ReactTestInstance) => { return ( isHostElement(element) && - !isHostTextInput(element) && isPointerEventEnabled(element) && !element.props.disabled && hasPressEventHandler From e2f07a39a759f6cccca545898e30facfc1d1b1b4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 7 Feb 2025 16:29:53 +0000 Subject: [PATCH 03/11] . --- .../react-native-gesture-handler.test.tsx | 29 ------------------- src/helpers/host-component-names.ts | 5 ---- 2 files changed, 34 deletions(-) diff --git a/src/__tests__/react-native-gesture-handler.test.tsx b/src/__tests__/react-native-gesture-handler.test.tsx index 2d6b57e0..644090f3 100644 --- a/src/__tests__/react-native-gesture-handler.test.tsx +++ b/src/__tests__/react-native-gesture-handler.test.tsx @@ -55,35 +55,6 @@ test('userEvent can invoke press events for RNGH Pressable', async () => { , ); - expect(screen.toJSON()).toMatchInlineSnapshot(` - - - - `); - const pressable = screen.getByTestId('pressable'); await user.press(pressable); expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); diff --git a/src/helpers/host-component-names.ts b/src/helpers/host-component-names.ts index 2e334ca6..45e019bc 100644 --- a/src/helpers/host-component-names.ts +++ b/src/helpers/host-component-names.ts @@ -17,11 +17,6 @@ export function isHostText(element: ReactTestInstance): element is HostTestInsta return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type); } -export type HostTextInput = HostTestInstance & { - type: 'TextInput'; - editable?: boolean; -}; - /** * Checks if the given element is a host TextInput element. * @param element The element to check. From 3d897097e6e587f12641d26fed23c093fe395a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 7 Feb 2025 18:40:17 +0100 Subject: [PATCH 04/11] . --- src/user-event/press/press.ts | 92 +++++++++++++++-------------------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 5ebd0b80..535356d6 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -3,9 +3,8 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; import { getEventHandler } from '../../event-handler'; import { getHostParent, isHostElement } from '../../helpers/component-tree'; -import { isHostTextInput } from '../../helpers/host-component-names'; +import { isHostText, isHostTextInput } from '../../helpers/host-component-names'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; -import { isEditableTextInput } from '../../helpers/text-input'; import { EventBuilder } from '../event-builder'; import type { UserEventConfig, UserEventInstance } from '../setup'; import { dispatchEvent, wait } from '../utils'; @@ -46,18 +45,13 @@ const basePress = async ( element: ReactTestInstance, options: BasePressOptions, ): Promise => { - if (isEditableTextInput(element) && isPointerEventEnabled(element)) { - await emitTextInputPressEvents(config, element, options); - return; - } - - if (!isHostTextInput(element) && isHostPressable(element)) { - await emitTextPressEvents(config, element, options); + if (isEnabledHostElement(element) && hasPressEventHandler(element)) { + await emitDirectPressEvents(config, element, options); return; } if (isEnabledTouchResponder(element)) { - await emitPressablePressEvents(config, element, options); + await emitPressabilityPressEvents(config, element, options); return; } @@ -69,53 +63,40 @@ const basePress = async ( await basePress(config, hostParentElement, options); }; -const emitPressablePressEvents = async ( - config: UserEventConfig, - element: ReactTestInstance, - options: BasePressOptions, -) => { - await wait(config); - - dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant()); - - const duration = options.duration ?? DEFAULT_MIN_PRESS_DURATION; - await wait(config, duration); +function isEnabledHostElement(element: ReactTestInstance) { + if (!isHostElement(element) || !isPointerEventEnabled(element)) { + return false; + } - dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease()); + if (isHostText(element)) { + return element.props.disabled !== true; + } - // 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 - duration > 0) { - await act(async () => { - await wait(config, DEFAULT_MIN_PRESS_DURATION - duration); - }); + if (isHostTextInput(element)) { + // @ts-expect-error - workaround incorrect ReactTestInstance type + return element.props.editable !== false; } -}; -const isEnabledTouchResponder = (element: ReactTestInstance) => { + return true; +} + +function isEnabledTouchResponder(element: ReactTestInstance) { return isPointerEventEnabled(element) && element.props.onStartShouldSetResponder?.(); -}; +} -const isHostPressable = (element: ReactTestInstance) => { - const hasPressEventHandler = +function hasPressEventHandler(element: ReactTestInstance) { + return ( getEventHandler(element, 'press') || getEventHandler(element, 'longPress') || getEventHandler(element, 'pressIn') || - getEventHandler(element, 'pressOut'); - - return ( - isHostElement(element) && - isPointerEventEnabled(element) && - !element.props.disabled && - hasPressEventHandler + getEventHandler(element, 'pressOut') ); -}; +} /** - * Dispatches a press event sequence for Text. + * Dispatches a press event sequence for host elements that have `onPress*` event handlers. */ -async function emitTextPressEvents( +async function emitDirectPressEvents( config: UserEventConfig, element: ReactTestInstance, options: BasePressOptions, @@ -141,19 +122,26 @@ async function emitTextPressEvents( } } -/** - * Dispatches a press event sequence for TextInput. - */ -async function emitTextInputPressEvents( +async function emitPressabilityPressEvents( config: UserEventConfig, element: ReactTestInstance, options: BasePressOptions, ) { await wait(config); - dispatchEvent(element, 'pressIn', EventBuilder.Common.touch()); - // Note: TextInput does not have `onPress`/`onLongPress` props. + dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant()); - await wait(config, options.duration); - dispatchEvent(element, 'pressOut', EventBuilder.Common.touch()); + const duration = 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 - duration > 0) { + await act(async () => { + await wait(config, DEFAULT_MIN_PRESS_DURATION - duration); + }); + } } From bf297dbeb2cc088a6ee2af010a24511c2d294ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 7 Feb 2025 19:19:14 +0100 Subject: [PATCH 05/11] . --- src/user-event/press/press.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 535356d6..abc0dc4c 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -2,7 +2,9 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; import { getEventHandler } from '../../event-handler'; +import type { HostTestInstance } from '../../helpers/component-tree'; import { getHostParent, isHostElement } from '../../helpers/component-tree'; +import { ErrorWithStack } from '../../helpers/errors'; import { isHostText, isHostTextInput } from '../../helpers/host-component-names'; import { isPointerEventEnabled } from '../../helpers/pointer-events'; import { EventBuilder } from '../event-builder'; @@ -19,6 +21,13 @@ export interface PressOptions { } export async function press(this: UserEventInstance, element: ReactTestInstance): Promise { + if (!isHostElement(element)) { + throw new ErrorWithStack( + `press() works only with host elements. Passed element has type "${element.type}".`, + press, + ); + } + await basePress(this.config, element, { type: 'press', }); @@ -29,6 +38,13 @@ export async function longPress( element: ReactTestInstance, options?: PressOptions, ): Promise { + if (!isHostElement(element)) { + throw new ErrorWithStack( + `longPress() works only with host elements. Passed element has type "${element.type}".`, + longPress, + ); + } + await basePress(this.config, element, { type: 'longPress', duration: options?.duration ?? DEFAULT_LONG_PRESS_DELAY_MS, @@ -42,7 +58,7 @@ interface BasePressOptions { const basePress = async ( config: UserEventConfig, - element: ReactTestInstance, + element: HostTestInstance, options: BasePressOptions, ): Promise => { if (isEnabledHostElement(element) && hasPressEventHandler(element)) { @@ -63,8 +79,8 @@ const basePress = async ( await basePress(config, hostParentElement, options); }; -function isEnabledHostElement(element: ReactTestInstance) { - if (!isHostElement(element) || !isPointerEventEnabled(element)) { +function isEnabledHostElement(element: HostTestInstance) { + if (!isPointerEventEnabled(element)) { return false; } @@ -80,11 +96,11 @@ function isEnabledHostElement(element: ReactTestInstance) { return true; } -function isEnabledTouchResponder(element: ReactTestInstance) { +function isEnabledTouchResponder(element: HostTestInstance) { return isPointerEventEnabled(element) && element.props.onStartShouldSetResponder?.(); } -function hasPressEventHandler(element: ReactTestInstance) { +function hasPressEventHandler(element: HostTestInstance) { return ( getEventHandler(element, 'press') || getEventHandler(element, 'longPress') || @@ -98,7 +114,7 @@ function hasPressEventHandler(element: ReactTestInstance) { */ async function emitDirectPressEvents( config: UserEventConfig, - element: ReactTestInstance, + element: HostTestInstance, options: BasePressOptions, ) { await wait(config); @@ -124,7 +140,7 @@ async function emitDirectPressEvents( async function emitPressabilityPressEvents( config: UserEventConfig, - element: ReactTestInstance, + element: HostTestInstance, options: BasePressOptions, ) { await wait(config); From b623c2cd340b2a6e56e0d6c179b6b4f565f7376a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 18 Feb 2025 10:25:01 +0100 Subject: [PATCH 06/11] . --- src/user-event/press/__tests__/press.real-timers.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 3e8550b5..903e353c 100644 --- a/src/user-event/press/__tests__/press.real-timers.test.tsx +++ b/src/user-event/press/__tests__/press.real-timers.test.tsx @@ -34,6 +34,8 @@ describe('userEvent.press with real timers', () => { ); await user.press(screen.getByTestId('pressable')); + // Typical event order is pressIn, pressOut, press + // But sometimes due to a race condition, the order is pressIn, press, pressOut. const eventSequence = getEventsNames(events).join(', '); expect( eventSequence === 'pressIn, pressOut, press' || eventSequence === 'pressIn, press, pressOut', @@ -201,11 +203,11 @@ describe('userEvent.press with real timers', () => { ); await user.press(screen.getByTestId('pressable')); - const eventsNames = getEventsNames(events).join(', '); + const eventSequence = getEventsNames(events).join(', '); // Typical event order is pressIn, pressOut, press // But sometimes due to a race condition, the order is pressIn, press, pressOut. expect( - eventsNames === 'pressIn, pressOut, press' || eventsNames === 'pressIn, press, pressOut', + eventSequence === 'pressIn, pressOut, press' || eventSequence === 'pressIn, press, pressOut', ).toBe(true); }); From f44e41b4344dfb266dd5ecdd065e3fcba16dfda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Feb 2025 09:02:02 +0100 Subject: [PATCH 07/11] code coverage --- .../press/__tests__/longPress.test.tsx | 34 ++++++++++++++++++- src/user-event/press/__tests__/press.test.tsx | 14 ++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index 9ae5f34d..910da2f1 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native'; +import { Pressable, Text, TouchableHighlight, TouchableOpacity, View } from 'react-native'; +import type { ReactTestInstance } from 'react-test-renderer'; import { render, screen } from '../../..'; import { createEventLogger, getEventsNames } from '../../../test-utils'; @@ -152,4 +153,35 @@ describe('userEvent.longPress with fake timers', () => { expect(mockOnLongPress).toHaveBeenCalled(); }); + + test('longPress accepts custom duration', async () => { + const { events, logEvent } = createEventLogger(); + const user = userEvent.setup(); + + render( + , + ); + + await user.longPress(screen.getByTestId('pressable'), { duration: 250 }); + expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); + }); + + it('longPress throws on composite components', async () => { + render(); + const user = userEvent.setup(); + + const compositeView = screen.getByTestId('view').parent as ReactTestInstance; + await expect(user.press(compositeView)).rejects.toThrowErrorMatchingInlineSnapshot(` + "press() works only with host elements. Passed element has type "function Component() { + (0, _classCallCheck2.default)(this, Component); + return _callSuper(this, Component, arguments); + }"." + `); + }); }); diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx index 2f805821..a73d9813 100644 --- a/src/user-event/press/__tests__/press.test.tsx +++ b/src/user-event/press/__tests__/press.test.tsx @@ -8,6 +8,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import type { ReactTestInstance } from 'react-test-renderer'; import { render, screen } from '../../..'; import { createEventLogger, getEventsNames } from '../../../test-utils'; @@ -331,6 +332,19 @@ describe('userEvent.press with fake timers', () => { expect(mockOnPress).toHaveBeenCalled(); }); + it('press throws on composite components', async () => { + render(); + const user = userEvent.setup(); + + const compositeView = screen.getByTestId('view').parent as ReactTestInstance; + await expect(user.press(compositeView)).rejects.toThrowErrorMatchingInlineSnapshot(` + "press() works only with host elements. Passed element has type "function Component() { + (0, _classCallCheck2.default)(this, Component); + return _callSuper(this, Component, arguments); + }"." + `); + }); + test('disables act environmennt', async () => { // In this test there is state update during await when typing // Since wait is not wrapped by act there would be a warning From 1249a5e806a1c82b97ff167109b3b773b99a1721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Feb 2025 09:13:54 +0100 Subject: [PATCH 08/11] . --- src/user-event/press/__tests__/longPress.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index 910da2f1..bcd3d4ec 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -177,8 +177,8 @@ describe('userEvent.longPress with fake timers', () => { const user = userEvent.setup(); const compositeView = screen.getByTestId('view').parent as ReactTestInstance; - await expect(user.press(compositeView)).rejects.toThrowErrorMatchingInlineSnapshot(` - "press() works only with host elements. Passed element has type "function Component() { + await expect(user.longPress(compositeView)).rejects.toThrowErrorMatchingInlineSnapshot(` + "longPress() works only with host elements. Passed element has type "function Component() { (0, _classCallCheck2.default)(this, Component); return _callSuper(this, Component, arguments); }"." From 548754640df81905b633a55809840a7e9367aa3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Feb 2025 09:19:51 +0100 Subject: [PATCH 09/11] . --- src/user-event/press/__tests__/longPress.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx index bcd3d4ec..3dfb2914 100644 --- a/src/user-event/press/__tests__/longPress.test.tsx +++ b/src/user-event/press/__tests__/longPress.test.tsx @@ -168,8 +168,8 @@ describe('userEvent.longPress with fake timers', () => { />, ); - await user.longPress(screen.getByTestId('pressable'), { duration: 250 }); - expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']); + await user.longPress(screen.getByTestId('pressable'), { duration: 50 }); + expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']); }); it('longPress throws on composite components', async () => { From bcf972e8fef45f275ccf03292972f97c0d0b7da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 20 Feb 2025 09:29:06 +0100 Subject: [PATCH 10/11] . --- examples/basic/components/__tests__/AnimatedView.test.tsx | 4 ++-- src/__tests__/react-native-animated.test.tsx | 4 ++-- src/user-event/press/press.ts | 4 +--- website/docs/12.x/docs/advanced/understanding-act.mdx | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/basic/components/__tests__/AnimatedView.test.tsx b/examples/basic/components/__tests__/AnimatedView.test.tsx index 0a4a3a29..25bf5ce8 100644 --- a/examples/basic/components/__tests__/AnimatedView.test.tsx +++ b/examples/basic/components/__tests__/AnimatedView.test.tsx @@ -20,7 +20,7 @@ describe('AnimatedView', () => { ); expect(screen.root).toHaveStyle({ opacity: 0 }); - await act(() => jest.advanceTimersByTime(250)); + act(() => jest.advanceTimersByTime(250)); expect(screen.root).toHaveStyle({ opacity: 1 }); }); @@ -32,7 +32,7 @@ describe('AnimatedView', () => { ); expect(screen.root).toHaveStyle({ opacity: 0 }); - await act(() => jest.advanceTimersByTime(250)); + act(() => jest.advanceTimersByTime(250)); expect(screen.root).toHaveStyle({ opacity: 1 }); }); }); diff --git a/src/__tests__/react-native-animated.test.tsx b/src/__tests__/react-native-animated.test.tsx index eb7b5ae5..85117f6b 100644 --- a/src/__tests__/react-native-animated.test.tsx +++ b/src/__tests__/react-native-animated.test.tsx @@ -51,7 +51,7 @@ describe('AnimatedView', () => { ); expect(screen.root).toHaveStyle({ opacity: 0 }); - await act(() => jest.advanceTimersByTime(250)); + act(() => jest.advanceTimersByTime(250)); // This stopped working in tests in RN 0.77 // expect(screen.root).toHaveStyle({ opacity: 0 }); }); @@ -64,7 +64,7 @@ describe('AnimatedView', () => { ); expect(screen.root).toHaveStyle({ opacity: 0 }); - await act(() => jest.advanceTimersByTime(250)); + act(() => jest.advanceTimersByTime(250)); expect(screen.root).toHaveStyle({ opacity: 1 }); }); }); diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index abc0dc4c..e0f43236 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -156,8 +156,6 @@ async function emitPressabilityPressEvents( // before emitting the `pressOut` event. We need to wait here, so that // `press()` function does not return before that. if (DEFAULT_MIN_PRESS_DURATION - duration > 0) { - await act(async () => { - await wait(config, DEFAULT_MIN_PRESS_DURATION - duration); - }); + await act(() => wait(config, DEFAULT_MIN_PRESS_DURATION - duration)); } } diff --git a/website/docs/12.x/docs/advanced/understanding-act.mdx b/website/docs/12.x/docs/advanced/understanding-act.mdx index a6dfd519..cb0f9f26 100644 --- a/website/docs/12.x/docs/advanced/understanding-act.mdx +++ b/website/docs/12.x/docs/advanced/understanding-act.mdx @@ -169,9 +169,7 @@ If we wanted to stick with real timers then things get a bit more complex. Let ```jsx test('render with real timers - sleep', async () => { render(); - await act(async () => { - await sleep(100); // Wait a bit longer than setTimeout in `TestAsyncComponent` - }); + await act(() => sleep(100)); // Wait a bit longer than setTimeout in `TestAsyncComponent` expect(screen.getByText('Count 1')).toBeOnTheScreen(); }); From 35d63d0194cd0c18c72e2cdaee9fd7aebe3f2af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 25 Feb 2025 17:18:57 +0100 Subject: [PATCH 11/11] . --- src/__tests__/react-native-animated.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/react-native-animated.test.tsx b/src/__tests__/react-native-animated.test.tsx index 85117f6b..389bde26 100644 --- a/src/__tests__/react-native-animated.test.tsx +++ b/src/__tests__/react-native-animated.test.tsx @@ -43,7 +43,7 @@ describe('AnimatedView', () => { jest.useRealTimers(); }); - it('should use native driver when useNativeDriver is true', async () => { + it('should use native driver when useNativeDriver is true', () => { render( Test @@ -56,7 +56,7 @@ describe('AnimatedView', () => { // expect(screen.root).toHaveStyle({ opacity: 0 }); }); - it('should not use native driver when useNativeDriver is false', async () => { + it('should not use native driver when useNativeDriver is false', () => { render( Test