diff --git a/src/fireEvent.ts b/src/fireEvent.ts
index a00dd7ac9..cf87ec9ff 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/pointer-events';
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/pointer-events.ts b/src/helpers/pointer-events.ts
new file mode 100644
index 000000000..fbd7495d3
--- /dev/null
+++ b/src/helpers/pointer-events.ts
@@ -0,0 +1,27 @@
+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 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
+): 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 isPointerEventEnabled(hostParent, true);
+};
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..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: [],
},
};
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/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx
new file mode 100644
index 000000000..fec234c0c
--- /dev/null
+++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Pressable, Text } from 'react-native';
+import { render, screen } from '../../../pure';
+import { userEvent } from '../..';
+import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';
+
+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 () => {
+ const mockOnLongPress = jest.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ press me
+
+ );
+ await user.longPress(screen.getByText('press me'));
+
+ expect(mockOnLongPress).toHaveBeenCalled();
+ });
+
+ test('calls onLongPress when duration is greater than specified delayLongPress', async () => {
+ const mockOnLongPress = jest.fn();
+ const mockOnPress = jest.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ press me
+
+ );
+
+ 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 delayLongPress', 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);
+ });
+
+ 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 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 () => {
+ jest.restoreAllMocks();
+ const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation();
+
+ render();
+
+ 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__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx
new file mode 100644
index 000000000..c9a23e00f
--- /dev/null
+++ b/src/user-event/press/__tests__/longPress.test.tsx
@@ -0,0 +1,157 @@
+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 { logEvent, events } = createEventLogger();
+ const user = userEvent.setup();
+
+ render(
+
+ press me
+
+ );
+ await user.longPress(screen.getByText('press me'));
+
+ 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": {
+ "changedTouches": [],
+ "identifier": 0,
+ "locationX": 0,
+ "locationY": 0,
+ "pageX": 0,
+ "pageY": 0,
+ "target": 0,
+ "timestamp": 0,
+ "touches": [],
+ },
+ "persist": [MockFunction] {
+ "calls": [
+ [],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ]
+ `);
+ });
+
+ test('calls onLongPress when duration is greater than specified delayLongPress', async () => {
+ const mockOnLongPress = jest.fn();
+ const mockOnPress = jest.fn();
+ const user = userEvent.setup();
+
+ render(
+
+ press me
+
+ );
+
+ 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 delayLongPress', 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);
+ });
+
+ 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 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/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx
new file mode 100644
index 000000000..fda9bf914
--- /dev/null
+++ b/src/user-event/press/__tests__/press.real-timers.test.tsx
@@ -0,0 +1,318 @@
+import * as React from 'react';
+import {
+ Pressable,
+ Text,
+ TextInput,
+ TouchableHighlight,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import { createEventLogger, getEventsName } from '../../../test-utils';
+import { render, screen } from '../../..';
+import { userEvent } from '../..';
+import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';
+
+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 () => {
+ const { events, logEvent } = createEventLogger();
+ const user = userEvent.setup();
+
+ render(
+
+ );
+ await user.press(screen.getByTestId('pressable'));
+
+ expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']);
+ });
+
+ 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'));
+
+ expect(events).toEqual([]);
+ });
+
+ test('does not call press when pointer events is none', async () => {
+ const { events, logEvent } = createEventLogger();
+ const user = userEvent.setup();
+
+ 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();
+
+ render(
+
+ );
+ await user.press(screen.getByTestId('pressable'));
+
+ expect(events).toEqual([]);
+ });
+
+ 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();
+
+ 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', 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('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();
+
+ 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('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();
+
+ render(
+
+ press me
+
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+});
+
+test('warns about using real timers with userEvent', async () => {
+ jest.restoreAllMocks();
+ const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation();
+
+ render();
+
+ 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/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx
index c7dda9f2a..defe7c615 100644
--- a/src/user-event/press/__tests__/press.test.tsx
+++ b/src/user-event/press/__tests__/press.test.tsx
@@ -1,67 +1,422 @@
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();
+ jest.setSystemTime(0);
+ });
-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(events).toMatchInlineSnapshot(`
+ [
+ {
+ "name": "pressIn",
+ "payload": {
+ "currentTarget": {
+ "measure": [MockFunction] {
+ "calls": [
+ [
+ [Function],
+ ],
+ [
+ [Function],
+ ],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ "dispatchConfig": {
+ "registrationName": "onResponderGrant",
+ },
+ "nativeEvent": {
+ "changedTouches": [],
+ "identifier": 0,
+ "locationX": 0,
+ "locationY": 0,
+ "pageX": 0,
+ "pageY": 0,
+ "target": 0,
+ "timestamp": 0,
+ "touches": [],
+ },
+ "persist": [MockFunction] {
+ "calls": [
+ [],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ {
+ "name": "press",
+ "payload": {
+ "currentTarget": {
+ "measure": [MockFunction],
+ },
+ "dispatchConfig": {
+ "registrationName": "onResponderRelease",
+ },
+ "nativeEvent": {
+ "changedTouches": [],
+ "identifier": 0,
+ "locationX": 0,
+ "locationY": 0,
+ "pageX": 0,
+ "pageY": 0,
+ "target": 0,
+ "timestamp": 0,
+ "touches": [],
+ },
+ "persist": [MockFunction] {
+ "calls": [
+ [],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ {
+ "name": "pressOut",
+ "payload": {
+ "currentTarget": {
+ "measure": [MockFunction],
+ },
+ "dispatchConfig": {
+ "registrationName": "onResponderRelease",
+ },
+ "nativeEvent": {
+ "changedTouches": [],
+ "identifier": 0,
+ "locationX": 0,
+ "locationY": 0,
+ "pageX": 0,
+ "pageY": 0,
+ "target": 0,
+ "timestamp": 0,
+ "touches": [],
+ },
+ "persist": [MockFunction] {
+ "calls": [
+ [],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ]
+ `);
+ });
+
+ test('does not trigger event when pressable is disabled', async () => {
+ const { events, logEvent } = createEventLogger();
+ const user = userEvent.setup();
- await user.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).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', 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('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();
+
+ 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('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();
+
+ 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/user-event/press/constants.ts b/src/user-event/press/constants.ts
new file mode 100644
index 000000000..8d237a0db
--- /dev/null
+++ b/src/user-event/press/constants.ts
@@ -0,0 +1,7 @@
+// 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/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..c69c04074 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -1,16 +1,134 @@
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 { isPointerEventEnabled } from '../../helpers/pointer-events';
+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 = {
+ duration: number;
+};
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 = { duration: 500 }
+): Promise {
+ await basePress(this.config, element, options);
}
+
+const basePress = async (
+ config: UserEventInstance['config'],
+ element: ReactTestInstance,
+ options: PressOptions = { duration: 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
+ if (isEnabledHostText(element) || isEnabledTextInput(element)) {
+ await triggerMockPressEvent(config, element, options);
+ return;
+ }
+
+ if (isEnabledTouchResponder(element)) {
+ await triggerPressEvent(config, element, options);
+ return;
+ }
+
+ const hostParentElement = getHostParent(element);
+ if (!hostParentElement) {
+ return;
+ }
+
+ await basePress(config, hostParentElement, options);
+};
+
+const triggerPressEvent = async (
+ config: UserEventInstance['config'],
+ element: ReactTestInstance,
+ options: PressOptions = { duration: 0 }
+) => {
+ const areFakeTimersEnabled = jestFakeTimersAreEnabled();
+ if (!areFakeTimersEnabled) {
+ warnAboutRealTimers();
+ }
+
+ await wait(config);
+
+ await act(async () => {
+ element.props.onResponderGrant({
+ ...EventBuilder.Common.touch(),
+ dispatchConfig: { registrationName: 'onResponderGrant' },
+ });
+
+ await wait(config, options.duration);
+
+ element.props.onResponderRelease({
+ ...EventBuilder.Common.touch(),
+ dispatchConfig: { registrationName: 'onResponderRelease' },
+ });
+
+ if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) {
+ await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration);
+ }
+ });
+};
+
+const isEnabledTouchResponder = (element: ReactTestInstance) => {
+ return (
+ isPointerEventEnabled(element) &&
+ 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());
+ }
+};
diff --git a/src/user-event/press/utils/warnAboutRealTimers.ts b/src/user-event/press/utils/warnAboutRealTimers.ts
new file mode 100644
index 000000000..307afaef3
--- /dev/null
+++ b/src/user-event/press/utils/warnAboutRealTimers.ts
@@ -0,0 +1,6 @@
+export const warnAboutRealTimers = () => {
+ // eslint-disable-next-line no-console
+ 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.`);
+};
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/website/docs/UserEvent.md b/website/docs/UserEvent.md
index 3ccdacab1..908002ea2 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
+press(
+ element: ReactTestInstance,
+): Promise
+```
+
+Example
+```ts
+const user = userEvent.setup();
+
+await user.press(element);
+```
+
+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()`
+
+```ts
+longPress(
+ element: ReactTestInstance,
+ options: { duration: number } = { duration: 500 }
+): Promise
+```
+
+Example
+```ts
+const user = userEvent.setup();
+
+await user.longPress(element);
+```
+
+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