Skip to content

Commit 1f38177

Browse files
pierrezimmermannbampierrezimmermannmdjastrzebski
authored
feat: userEvent.press (#1386)
* feat: create first version of userEvent.press * feat: do not trigger press event when pointer events is disabled * feat: make press events bubble up when trigerring a non touch responder * tests: add test cases for userevent.press to check calls to onPressIn and onPressOut * refactor: move userEvent.press tests in a dedicated test file * feat: add pressDuration option for userEvent.press * refactor: group test that check prop calls * feat: add support for touchable opacity for userevent press * feat: add support for Text * feat: add support for TextInput for userEvent.press * refactor: add some comments to explain pointer events api * refactor: change the api of userEvent.press to make it async * feat: add support for real timers for userEvent.press * refactor: create a longPress api and remove duration option from press api * feat: add warning for users when userEvent is used with real timers * refactor: rewrite press using common user event code * refactor: remove duplicate pointerEventEnabled method * refactor: remove check on fake timers in user.press * refactor: change order of functions in press file to have exports first * feat: add delay before press * feat: wait min press duration before calling onPressout for text and textInput * chore: improve coverage * feat: account for press duration when waiting for press out * fix: wait for press duration also when pressing text or textinput * docs: add documentation on press and longpress * fix: check pointer events for Text and TextInput * chore: fixes and tweaks on userEvent docs * Update press doc based on review suggestion Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com> * refactor: rename pressDuration option to duration * Update longPress doc based on review suggestion Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com> * refactor: use ts doc for isPointerEventsEnabled method * refactor: rename file isPointerEventsEnabled to pointer-events * refactor: split test in longPress in two * refactor: use Date.now instead of new Date().getTime() * refactor: also test payload of events for press and longpress * refactor: fix typo in some longpress test names * Update src/user-event/press/utils/warnAboutRealTimers.ts Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com> * refactor: merge press and touch events * fix: return after pressing text or textinput * refactor: extract functions to trigger press on text/textInput * refactor: use optional chaining * feat: update warning when using userEvent with real timers * refactor: check on press test that longPress is not called * refactor: test directly warnings logged with real timers without mocking function --------- Co-authored-by: pierrezimmermann <pierrez@nam.tech> Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
1 parent 1c0fc79 commit 1f38177

17 files changed

+1197
-117
lines changed

src/fireEvent.ts

+2-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import act from './act';
3-
import { getHostParent, isHostElement } from './helpers/component-tree';
3+
import { isHostElement } from './helpers/component-tree';
44
import { getHostComponentNames } from './helpers/host-component-names';
5+
import { isPointerEventEnabled } from './helpers/pointer-events';
56

67
type EventHandler = (...args: unknown[]) => unknown;
78

@@ -19,27 +20,6 @@ export function isTouchResponder(element: ReactTestInstance) {
1920
);
2021
}
2122

22-
export function isPointerEventEnabled(
23-
element: ReactTestInstance,
24-
isParent?: boolean
25-
): boolean {
26-
const pointerEvents = element.props.pointerEvents;
27-
if (pointerEvents === 'none') {
28-
return false;
29-
}
30-
31-
if (isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none') {
32-
return false;
33-
}
34-
35-
const parent = getHostParent(element);
36-
if (!parent) {
37-
return true;
38-
}
39-
40-
return isPointerEventEnabled(parent, true);
41-
}
42-
4323
/**
4424
* List of events affected by `pointerEvents` prop.
4525
*

src/helpers/pointer-events.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import { getHostParent } from './component-tree';
3+
4+
/**
5+
* pointerEvents controls whether the View can be the target of touch events.
6+
* 'auto': The View and its children can be the target of touch events.
7+
* 'none': The View is never the target of touch events.
8+
* 'box-none': The View is never the target of touch events but its subviews can be
9+
* 'box-only': The view can be the target of touch events but its subviews cannot be
10+
* see the official react native doc https://reactnative.dev/docs/view#pointerevents */
11+
export const isPointerEventEnabled = (
12+
element: ReactTestInstance,
13+
isParent?: boolean
14+
): boolean => {
15+
const parentCondition = isParent
16+
? element?.props.pointerEvents === 'box-only'
17+
: element?.props.pointerEvents === 'box-none';
18+
19+
if (element?.props.pointerEvents === 'none' || parentCondition) {
20+
return false;
21+
}
22+
23+
const hostParent = getHostParent(element);
24+
if (!hostParent) return true;
25+
26+
return isPointerEventEnabled(hostParent, true);
27+
};

src/test-utils/events.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ export function createEventLogger() {
1818

1919
return { events, logEvent };
2020
}
21+
22+
export function getEventsName(events: EventEntry[]) {
23+
return events.map((event) => event.name);
24+
}

src/user-event/event-builder/common.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export const CommonEventBuilder = {
66
*/
77
touch: () => {
88
return {
9+
persist: jest.fn(),
10+
currentTarget: { measure: jest.fn() },
911
nativeEvent: {
1012
changedTouches: [],
1113
identifier: 0,
@@ -14,7 +16,7 @@ export const CommonEventBuilder = {
1416
pageX: 0,
1517
pageY: 0,
1618
target: 0,
17-
timestamp: 0,
19+
timestamp: Date.now(),
1820
touches: [],
1921
},
2022
};

src/user-event/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { setup } from './setup';
3+
import { PressOptions } from './press/press';
34

45
export const userEvent = {
56
setup,
67

78
// Direct access for User Event v13 compatibility
89
press: (element: ReactTestInstance) => setup().press(element),
10+
longPress: (element: ReactTestInstance, options?: PressOptions) =>
11+
setup().longPress(element, options),
912
type: (element: ReactTestInstance, text: string) =>
1013
setup().type(element, text),
1114
};

src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap

-54
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React from 'react';
2+
import { Pressable, Text } from 'react-native';
3+
import { render, screen } from '../../../pure';
4+
import { userEvent } from '../..';
5+
import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';
6+
7+
describe('userEvent.longPress with real timers', () => {
8+
beforeEach(() => {
9+
jest.useRealTimers();
10+
jest.restoreAllMocks();
11+
jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation();
12+
});
13+
14+
test('calls onLongPress if the delayLongPress is the default one', async () => {
15+
const mockOnLongPress = jest.fn();
16+
const user = userEvent.setup();
17+
18+
render(
19+
<Pressable onLongPress={mockOnLongPress}>
20+
<Text>press me</Text>
21+
</Pressable>
22+
);
23+
await user.longPress(screen.getByText('press me'));
24+
25+
expect(mockOnLongPress).toHaveBeenCalled();
26+
});
27+
28+
test('calls onLongPress when duration is greater than specified delayLongPress', async () => {
29+
const mockOnLongPress = jest.fn();
30+
const mockOnPress = jest.fn();
31+
const user = userEvent.setup();
32+
33+
render(
34+
<Pressable
35+
delayLongPress={800}
36+
onLongPress={mockOnLongPress}
37+
onPress={mockOnPress}
38+
>
39+
<Text>press me</Text>
40+
</Pressable>
41+
);
42+
43+
await user.longPress(screen.getByText('press me'), {
44+
duration: 1000,
45+
});
46+
47+
expect(mockOnLongPress).toHaveBeenCalled();
48+
expect(mockOnPress).not.toHaveBeenCalled();
49+
});
50+
51+
test('does not calls onLongPress when duration is lesser than specified delayLongPress', async () => {
52+
const mockOnLongPress = jest.fn();
53+
const mockOnPress = jest.fn();
54+
const user = userEvent.setup();
55+
56+
render(
57+
<Pressable
58+
delayLongPress={1000}
59+
onLongPress={mockOnLongPress}
60+
onPress={mockOnPress}
61+
>
62+
<Text>press me</Text>
63+
</Pressable>
64+
);
65+
await user.longPress(screen.getByText('press me'));
66+
67+
expect(mockOnLongPress).not.toHaveBeenCalled();
68+
expect(mockOnPress).toHaveBeenCalledTimes(1);
69+
});
70+
71+
test('does not calls onPress when onLongPress is called', async () => {
72+
const mockOnLongPress = jest.fn();
73+
const mockOnPress = jest.fn();
74+
const user = userEvent.setup();
75+
76+
render(
77+
<Pressable onLongPress={mockOnLongPress} onPress={mockOnPress}>
78+
<Text>press me</Text>
79+
</Pressable>
80+
);
81+
await user.longPress(screen.getByText('press me'));
82+
83+
expect(mockOnLongPress).toHaveBeenCalled();
84+
expect(mockOnPress).not.toHaveBeenCalled();
85+
});
86+
87+
test('longPress is accessible directly in userEvent', async () => {
88+
const mockOnLongPress = jest.fn();
89+
90+
render(
91+
<Pressable onLongPress={mockOnLongPress}>
92+
<Text>press me</Text>
93+
</Pressable>
94+
);
95+
96+
await userEvent.longPress(screen.getByText('press me'));
97+
98+
expect(mockOnLongPress).toHaveBeenCalled();
99+
});
100+
});
101+
102+
test('warns about using real timers with userEvent', async () => {
103+
jest.restoreAllMocks();
104+
const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation();
105+
106+
render(<Pressable testID="pressable" />);
107+
108+
await userEvent.longPress(screen.getByTestId('pressable'));
109+
110+
expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(`
111+
"It is recommended to use userEvent with fake timers
112+
Some events involve duration so your tests may take a long time to run.
113+
For instance calling userEvent.longPress with real timers will take 500 ms."
114+
`);
115+
});

0 commit comments

Comments
 (0)