mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
fix: RN Gesture Handler Pressable support (#1746)
This commit is contained in:
committed by
GitHub
parent
a9c056bd02
commit
bd78a26b2a
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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(
|
||||
<AnimatedView fadeInDuration={250} useNativeDriver={true}>
|
||||
Test
|
||||
@@ -51,12 +51,12 @@ 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 });
|
||||
});
|
||||
|
||||
it('should not use native driver when useNativeDriver is false', async () => {
|
||||
it('should not use native driver when useNativeDriver is false', () => {
|
||||
render(
|
||||
<AnimatedView fadeInDuration={250} useNativeDriver={false}>
|
||||
Test
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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(
|
||||
<View>
|
||||
<Pressable
|
||||
testID="pressable"
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
</View>,
|
||||
);
|
||||
|
||||
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(
|
||||
<View>
|
||||
<Pressable
|
||||
testID="pressable"
|
||||
onPress={logEvent('press')}
|
||||
onPressIn={logEvent('pressIn')}
|
||||
onPressOut={logEvent('pressOut')}
|
||||
onLongPress={logEvent('longPress')}
|
||||
/>
|
||||
</View>,
|
||||
);
|
||||
|
||||
const pressable = screen.getByTestId('pressable');
|
||||
await user.press(pressable);
|
||||
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
|
||||
});
|
||||
@@ -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(
|
||||
<Pressable
|
||||
onPress={logEvent('press')}
|
||||
onPressIn={logEvent('pressIn')}
|
||||
onPressOut={logEvent('pressOut')}
|
||||
onLongPress={logEvent('longPress')}
|
||||
testID="pressable"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.longPress(screen.getByTestId('pressable'), { duration: 50 });
|
||||
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
|
||||
});
|
||||
|
||||
it('longPress throws on composite components', async () => {
|
||||
render(<View testID="view" />);
|
||||
const user = userEvent.setup();
|
||||
|
||||
const compositeView = screen.getByTestId('view').parent as ReactTestInstance;
|
||||
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);
|
||||
}"."
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(<View testID="view" />);
|
||||
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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 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 { isEditableTextInput } from '../../helpers/text-input';
|
||||
import { EventBuilder } from '../event-builder';
|
||||
import type { UserEventConfig, UserEventInstance } from '../setup';
|
||||
import { dispatchEvent, wait } from '../utils';
|
||||
@@ -19,6 +21,13 @@ export interface PressOptions {
|
||||
}
|
||||
|
||||
export async function press(this: UserEventInstance, element: ReactTestInstance): Promise<void> {
|
||||
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<void> {
|
||||
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,21 +58,16 @@ interface BasePressOptions {
|
||||
|
||||
const basePress = async (
|
||||
config: UserEventConfig,
|
||||
element: ReactTestInstance,
|
||||
element: HostTestInstance,
|
||||
options: BasePressOptions,
|
||||
): Promise<void> => {
|
||||
if (isPressableText(element)) {
|
||||
await emitTextPressEvents(config, element, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditableTextInput(element) && isPointerEventEnabled(element)) {
|
||||
await emitTextInputPressEvents(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;
|
||||
}
|
||||
|
||||
@@ -68,56 +79,42 @@ 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);
|
||||
|
||||
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);
|
||||
});
|
||||
function isEnabledHostElement(element: HostTestInstance) {
|
||||
if (!isPointerEventEnabled(element)) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isEnabledTouchResponder = (element: ReactTestInstance) => {
|
||||
if (isHostText(element)) {
|
||||
return element.props.disabled !== true;
|
||||
}
|
||||
|
||||
if (isHostTextInput(element)) {
|
||||
// @ts-expect-error - workaround incorrect ReactTestInstance type
|
||||
return element.props.editable !== false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isEnabledTouchResponder(element: HostTestInstance) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
function hasPressEventHandler(element: HostTestInstance) {
|
||||
return (
|
||||
isHostText(element) &&
|
||||
isPointerEventEnabled(element) &&
|
||||
!element.props.disabled &&
|
||||
hasPressEventHandler
|
||||
getEventHandler(element, 'press') ||
|
||||
getEventHandler(element, 'longPress') ||
|
||||
getEventHandler(element, 'pressIn') ||
|
||||
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,
|
||||
element: HostTestInstance,
|
||||
options: BasePressOptions,
|
||||
) {
|
||||
await wait(config);
|
||||
@@ -141,19 +138,24 @@ async function emitTextPressEvents(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a press event sequence for TextInput.
|
||||
*/
|
||||
async function emitTextInputPressEvents(
|
||||
async function emitPressabilityPressEvents(
|
||||
config: UserEventConfig,
|
||||
element: ReactTestInstance,
|
||||
element: HostTestInstance,
|
||||
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(() => wait(config, DEFAULT_MIN_PRESS_DURATION - duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(<TestAsyncComponent />);
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user