Skip to content

Commit 5de8790

Browse files
authoredOct 30, 2024··
fix: press event order (#1696)
1 parent 0632000 commit 5de8790

15 files changed

+534
-182
lines changed
 

‎experiments-app/src/experiments.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AccessibilityScreen } from './screens/Accessibility';
2+
import { PressEvents } from './screens/PressEvents';
23
import { TextInputEventPropagation } from './screens/TextInputEventPropagation';
34
import { TextInputEvents } from './screens/TextInputEvents';
45
import { ScrollViewEvents } from './screens/ScrollViewEvents';
@@ -13,6 +14,11 @@ export const experiments = [
1314
title: 'Accessibility',
1415
component: AccessibilityScreen,
1516
},
17+
{
18+
key: 'PressEvents',
19+
title: 'Press Events',
20+
component: PressEvents,
21+
},
1622
{
1723
key: 'TextInputEvents',
1824
title: 'TextInput Events',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as React from 'react';
2+
import {
3+
StyleSheet,
4+
SafeAreaView,
5+
Text,
6+
TextInput,
7+
View,
8+
Pressable,
9+
TouchableOpacity,
10+
} from 'react-native';
11+
import { nativeEventLogger, logEvent } from '../utils/helpers';
12+
13+
export function PressEvents() {
14+
const [value, setValue] = React.useState('');
15+
16+
const handleChangeText = (value: string) => {
17+
setValue(value);
18+
logEvent('changeText', value);
19+
};
20+
21+
return (
22+
<SafeAreaView style={styles.container}>
23+
<View style={styles.wrapper}>
24+
<TextInput
25+
style={styles.textInput}
26+
value={value}
27+
onPress={nativeEventLogger('press')}
28+
onPressIn={nativeEventLogger('pressIn')}
29+
onPressOut={nativeEventLogger('pressOut')}
30+
/>
31+
</View>
32+
<View style={styles.wrapper}>
33+
<Text
34+
onPress={nativeEventLogger('press')}
35+
onLongPress={nativeEventLogger('longPress')}
36+
onPressIn={nativeEventLogger('pressIn')}
37+
onPressOut={nativeEventLogger('pressOut')}
38+
>
39+
Text
40+
</Text>
41+
</View>
42+
<View style={styles.wrapper}>
43+
<Pressable
44+
onPress={nativeEventLogger('press')}
45+
onLongPress={nativeEventLogger('longPress')}
46+
onPressIn={nativeEventLogger('pressIn')}
47+
onPressOut={nativeEventLogger('pressOut')}
48+
>
49+
<Text>Pressable</Text>
50+
</Pressable>
51+
</View>
52+
<View style={styles.wrapper}>
53+
<TouchableOpacity
54+
onPress={nativeEventLogger('press')}
55+
onLongPress={nativeEventLogger('longPress')}
56+
onPressIn={nativeEventLogger('pressIn')}
57+
onPressOut={nativeEventLogger('pressOut')}
58+
>
59+
<Text>Pressable</Text>
60+
</TouchableOpacity>
61+
</View>
62+
</SafeAreaView>
63+
);
64+
}
65+
66+
const styles = StyleSheet.create({
67+
container: {
68+
flex: 1,
69+
},
70+
wrapper: {
71+
padding: 20,
72+
backgroundColor: 'yellow',
73+
},
74+
textInput: {
75+
backgroundColor: 'white',
76+
margin: 20,
77+
padding: 8,
78+
fontSize: 18,
79+
borderWidth: 1,
80+
borderColor: 'grey',
81+
},
82+
});

‎experiments-app/src/utils/helpers.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { NativeSyntheticEvent } from 'react-native/types';
22

3+
let lastEventTimeStamp: number | null = null;
4+
35
export function nativeEventLogger(name: string) {
46
return (event: NativeSyntheticEvent<unknown>) => {
57
logEvent(name, event?.nativeEvent);
@@ -14,5 +16,6 @@ export function customEventLogger(name: string) {
1416

1517
export function logEvent(name: string, ...args: unknown[]) {
1618
// eslint-disable-next-line no-console
17-
console.log(`Event: ${name}`, ...args);
19+
console.log(`[${Date.now() - (lastEventTimeStamp ?? Date.now())}ms] Event: ${name}`, ...args);
20+
lastEventTimeStamp = Date.now();
1821
}

‎src/__tests__/render.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -249,5 +249,5 @@ test('supports legacy rendering', () => {
249249

250250
test('supports concurrent rendering', () => {
251251
render(<View testID="test" />, { concurrentRoot: true });
252-
expect(screen.root).toBeDefined();
252+
expect(screen.root).toBeOnTheScreen();
253253
});

‎src/fire-event.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ScrollViewProps,
88
} from 'react-native';
99
import act from './act';
10-
import { isHostElement } from './helpers/component-tree';
10+
import { isElementMounted, isHostElement } from './helpers/component-tree';
1111
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
1212
import { isPointerEventEnabled } from './helpers/pointer-events';
1313
import { isTextInputEditable } from './helpers/text-input';
@@ -121,6 +121,10 @@ type EventName = StringWithAutocomplete<
121121
>;
122122

123123
function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) {
124+
if (!isElementMounted(element)) {
125+
return;
126+
}
127+
124128
setNativeStateIfNeeded(element, eventName, data[0]);
125129

126130
const handler = findEventHandler(element, eventName);

‎src/helpers/component-tree.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactTestInstance } from 'react-test-renderer';
2-
2+
import { screen } from '../screen';
33
/**
44
* ReactTestInstance referring to host element.
55
*/
@@ -13,6 +13,10 @@ export function isHostElement(element?: ReactTestInstance | null): element is Ho
1313
return typeof element?.type === 'string';
1414
}
1515

16+
export function isElementMounted(element: ReactTestInstance | null) {
17+
return getUnsafeRootElement(element) === screen.UNSAFE_root;
18+
}
19+
1620
/**
1721
* Returns first host ancestor for given element.
1822
* @param element The element start traversing from.

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

+95
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,98 @@ exports[`userEvent.longPress with fake timers calls onLongPress if the delayLong
3434
},
3535
]
3636
`;
37+
38+
exports[`userEvent.longPress with fake timers works on Pressable 1`] = `
39+
[
40+
{
41+
"name": "pressIn",
42+
"payload": {
43+
"currentTarget": {
44+
"measure": [Function],
45+
},
46+
"dispatchConfig": {
47+
"registrationName": "onResponderGrant",
48+
},
49+
"isDefaultPrevented": [Function],
50+
"isPersistent": [Function],
51+
"isPropagationStopped": [Function],
52+
"nativeEvent": {
53+
"changedTouches": [],
54+
"identifier": 0,
55+
"locationX": 0,
56+
"locationY": 0,
57+
"pageX": 0,
58+
"pageY": 0,
59+
"target": 0,
60+
"timestamp": 0,
61+
"touches": [],
62+
},
63+
"persist": [Function],
64+
"preventDefault": [Function],
65+
"stopPropagation": [Function],
66+
"target": {},
67+
"timeStamp": 0,
68+
},
69+
},
70+
{
71+
"name": "longPress",
72+
"payload": {
73+
"currentTarget": {
74+
"measure": [Function],
75+
},
76+
"dispatchConfig": {
77+
"registrationName": "onResponderGrant",
78+
},
79+
"isDefaultPrevented": [Function],
80+
"isPersistent": [Function],
81+
"isPropagationStopped": [Function],
82+
"nativeEvent": {
83+
"changedTouches": [],
84+
"identifier": 0,
85+
"locationX": 0,
86+
"locationY": 0,
87+
"pageX": 0,
88+
"pageY": 0,
89+
"target": 0,
90+
"timestamp": 0,
91+
"touches": [],
92+
},
93+
"persist": [Function],
94+
"preventDefault": [Function],
95+
"stopPropagation": [Function],
96+
"target": {},
97+
"timeStamp": 0,
98+
},
99+
},
100+
{
101+
"name": "pressOut",
102+
"payload": {
103+
"currentTarget": {
104+
"measure": [Function],
105+
},
106+
"dispatchConfig": {
107+
"registrationName": "onResponderRelease",
108+
},
109+
"isDefaultPrevented": [Function],
110+
"isPersistent": [Function],
111+
"isPropagationStopped": [Function],
112+
"nativeEvent": {
113+
"changedTouches": [],
114+
"identifier": 0,
115+
"locationX": 0,
116+
"locationY": 0,
117+
"pageX": 0,
118+
"pageY": 0,
119+
"target": 0,
120+
"timestamp": 500,
121+
"touches": [],
122+
},
123+
"persist": [Function],
124+
"preventDefault": [Function],
125+
"stopPropagation": [Function],
126+
"target": {},
127+
"timeStamp": 0,
128+
},
129+
},
130+
]
131+
`;

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOut prop of touchable 1`] = `
3+
exports[`userEvent.press with fake timers works on Pressable 1`] = `
44
[
55
{
66
"name": "pressIn",
@@ -33,7 +33,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
3333
},
3434
},
3535
{
36-
"name": "press",
36+
"name": "pressOut",
3737
"payload": {
3838
"currentTarget": {
3939
"measure": [Function],
@@ -52,7 +52,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
5252
"pageX": 0,
5353
"pageY": 0,
5454
"target": 0,
55-
"timestamp": 0,
55+
"timestamp": 130,
5656
"touches": [],
5757
},
5858
"persist": [Function],
@@ -63,7 +63,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
6363
},
6464
},
6565
{
66-
"name": "pressOut",
66+
"name": "press",
6767
"payload": {
6868
"currentTarget": {
6969
"measure": [Function],
@@ -82,7 +82,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
8282
"pageX": 0,
8383
"pageY": 0,
8484
"target": 0,
85-
"timestamp": 0,
85+
"timestamp": 130,
8686
"touches": [],
8787
},
8888
"persist": [Function],

‎src/user-event/press/__tests__/longPress.real-timers.test.tsx

+65-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
2-
import { Pressable, Text } from 'react-native';
3-
import { render, screen } from '../../../pure';
2+
import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native';
3+
import { createEventLogger, getEventsNames } from '../../../test-utils';
4+
import { render, screen } from '../../..';
45
import { userEvent } from '../..';
56

67
describe('userEvent.longPress with real timers', () => {
@@ -9,6 +10,68 @@ describe('userEvent.longPress with real timers', () => {
910
jest.restoreAllMocks();
1011
});
1112

13+
test('works on Pressable', async () => {
14+
const { events, logEvent } = createEventLogger();
15+
const user = userEvent.setup();
16+
17+
render(
18+
<Pressable
19+
onPress={logEvent('press')}
20+
onPressIn={logEvent('pressIn')}
21+
onPressOut={logEvent('pressOut')}
22+
onLongPress={logEvent('longPress')}
23+
testID="pressable"
24+
/>,
25+
);
26+
27+
await user.longPress(screen.getByTestId('pressable'));
28+
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
29+
});
30+
31+
test('works on TouchableOpacity', async () => {
32+
const mockOnPress = jest.fn();
33+
34+
render(
35+
<TouchableOpacity onPress={mockOnPress}>
36+
<Text>press me</Text>
37+
</TouchableOpacity>,
38+
);
39+
40+
await userEvent.longPress(screen.getByText('press me'));
41+
expect(mockOnPress).toHaveBeenCalled();
42+
});
43+
44+
test('works on TouchableHighlight', async () => {
45+
const mockOnPress = jest.fn();
46+
47+
render(
48+
<TouchableHighlight onPress={mockOnPress}>
49+
<Text>press me</Text>
50+
</TouchableHighlight>,
51+
);
52+
53+
await userEvent.longPress(screen.getByText('press me'));
54+
expect(mockOnPress).toHaveBeenCalled();
55+
});
56+
57+
test('works on Text', async () => {
58+
const { events, logEvent } = createEventLogger();
59+
60+
render(
61+
<Text
62+
onPress={logEvent('press')}
63+
onPressIn={logEvent('pressIn')}
64+
onPressOut={logEvent('pressOut')}
65+
onLongPress={logEvent('longPress')}
66+
>
67+
press me
68+
</Text>,
69+
);
70+
71+
await userEvent.longPress(screen.getByText('press me'));
72+
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
73+
});
74+
1275
test('calls onLongPress if the delayLongPress is the default one', async () => {
1376
const mockOnLongPress = jest.fn();
1477
const user = userEvent.setup();

‎src/user-event/press/__tests__/longPress.test.tsx

+66-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,78 @@
11
import React from 'react';
2-
import { Pressable, Text } from 'react-native';
3-
import { render, screen } from '../../../pure';
2+
import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native';
3+
import { createEventLogger, getEventsNames } from '../../../test-utils';
4+
import { render, screen } from '../../..';
45
import { userEvent } from '../..';
5-
import { createEventLogger } from '../../../test-utils';
66

77
describe('userEvent.longPress with fake timers', () => {
88
beforeEach(() => {
99
jest.useFakeTimers();
1010
jest.setSystemTime(0);
1111
});
1212

13+
test('works on Pressable', async () => {
14+
const { events, logEvent } = createEventLogger();
15+
const user = userEvent.setup();
16+
17+
render(
18+
<Pressable
19+
onPress={logEvent('press')}
20+
onPressIn={logEvent('pressIn')}
21+
onPressOut={logEvent('pressOut')}
22+
onLongPress={logEvent('longPress')}
23+
testID="pressable"
24+
/>,
25+
);
26+
27+
await user.longPress(screen.getByTestId('pressable'));
28+
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
29+
expect(events).toMatchSnapshot();
30+
});
31+
32+
test('works on TouchableOpacity', async () => {
33+
const mockOnPress = jest.fn();
34+
35+
render(
36+
<TouchableOpacity onPress={mockOnPress}>
37+
<Text>press me</Text>
38+
</TouchableOpacity>,
39+
);
40+
41+
await userEvent.longPress(screen.getByText('press me'));
42+
expect(mockOnPress).toHaveBeenCalled();
43+
});
44+
45+
test('works on TouchableHighlight', async () => {
46+
const mockOnPress = jest.fn();
47+
48+
render(
49+
<TouchableHighlight onPress={mockOnPress}>
50+
<Text>press me</Text>
51+
</TouchableHighlight>,
52+
);
53+
54+
await userEvent.longPress(screen.getByText('press me'));
55+
expect(mockOnPress).toHaveBeenCalled();
56+
});
57+
58+
test('works on Text', async () => {
59+
const { events, logEvent } = createEventLogger();
60+
61+
render(
62+
<Text
63+
onPress={logEvent('press')}
64+
onPressIn={logEvent('pressIn')}
65+
onPressOut={logEvent('pressOut')}
66+
onLongPress={logEvent('longPress')}
67+
>
68+
press me
69+
</Text>,
70+
);
71+
72+
await userEvent.longPress(screen.getByText('press me'));
73+
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
74+
});
75+
1376
test('calls onLongPress if the delayLongPress is the default one', async () => {
1477
const { logEvent, events } = createEventLogger();
1578
const user = userEvent.setup();

‎src/user-event/press/__tests__/press.real-timers.test.tsx

+74-65
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import {
3+
Button,
34
Pressable,
45
Text,
56
TextInput,
@@ -17,7 +18,7 @@ describe('userEvent.press with real timers', () => {
1718
jest.restoreAllMocks();
1819
});
1920

20-
test('calls onPressIn, onPress and onPressOut prop of touchable', async () => {
21+
test('works on Pressable', async () => {
2122
const { events, logEvent } = createEventLogger();
2223
const user = userEvent.setup();
2324

@@ -30,9 +31,77 @@ describe('userEvent.press with real timers', () => {
3031
testID="pressable"
3132
/>,
3233
);
34+
3335
await user.press(screen.getByTestId('pressable'));
36+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
37+
});
38+
39+
test('works on TouchableOpacity', async () => {
40+
const mockOnPress = jest.fn();
41+
42+
render(
43+
<TouchableOpacity onPress={mockOnPress}>
44+
<Text>press me</Text>
45+
</TouchableOpacity>,
46+
);
3447

35-
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
48+
await userEvent.press(screen.getByText('press me'));
49+
expect(mockOnPress).toHaveBeenCalled();
50+
});
51+
52+
test('works on TouchableHighlight', async () => {
53+
const mockOnPress = jest.fn();
54+
55+
render(
56+
<TouchableHighlight onPress={mockOnPress}>
57+
<Text>press me</Text>
58+
</TouchableHighlight>,
59+
);
60+
61+
await userEvent.press(screen.getByText('press me'));
62+
expect(mockOnPress).toHaveBeenCalled();
63+
});
64+
65+
test('works on Text', async () => {
66+
const { events, logEvent } = createEventLogger();
67+
68+
render(
69+
<Text
70+
onPress={logEvent('press')}
71+
onPressIn={logEvent('pressIn')}
72+
onPressOut={logEvent('pressOut')}
73+
onLongPress={logEvent('longPress')}
74+
>
75+
press me
76+
</Text>,
77+
);
78+
79+
await userEvent.press(screen.getByText('press me'));
80+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
81+
});
82+
83+
test('works on TextInput', async () => {
84+
const { events, logEvent } = createEventLogger();
85+
86+
render(
87+
<TextInput
88+
placeholder="email"
89+
onPressIn={logEvent('pressIn')}
90+
onPressOut={logEvent('pressOut')}
91+
/>,
92+
);
93+
94+
await userEvent.press(screen.getByPlaceholderText('email'));
95+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
96+
});
97+
98+
test('works on Button', async () => {
99+
const { events, logEvent } = createEventLogger();
100+
101+
render(<Button title="press me" onPress={logEvent('press')} />);
102+
103+
await userEvent.press(screen.getByText('press me'));
104+
expect(getEventsNames(events)).toEqual(['press']);
36105
});
37106

38107
test('does not trigger event when pressable is disabled', async () => {
@@ -128,7 +197,7 @@ describe('userEvent.press with real timers', () => {
128197
);
129198
await user.press(screen.getByTestId('pressable'));
130199

131-
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
200+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
132201
});
133202

134203
test('crawls up in the tree to find an element that responds to touch events', async () => {
@@ -157,50 +226,6 @@ describe('userEvent.press with real timers', () => {
157226
expect(mockOnLongPress).not.toHaveBeenCalled();
158227
});
159228

160-
test('works on TouchableOpacity', async () => {
161-
const mockOnPress = jest.fn();
162-
163-
render(
164-
<TouchableOpacity onPress={mockOnPress}>
165-
<Text>press me</Text>
166-
</TouchableOpacity>,
167-
);
168-
await userEvent.press(screen.getByText('press me'));
169-
170-
expect(mockOnPress).toHaveBeenCalled();
171-
});
172-
173-
test('works on TouchableHighlight', async () => {
174-
const mockOnPress = jest.fn();
175-
176-
render(
177-
<TouchableHighlight onPress={mockOnPress}>
178-
<Text>press me</Text>
179-
</TouchableHighlight>,
180-
);
181-
await userEvent.press(screen.getByText('press me'));
182-
183-
expect(mockOnPress).toHaveBeenCalled();
184-
});
185-
186-
test('works on Text', async () => {
187-
const { events, logEvent } = createEventLogger();
188-
189-
render(
190-
<Text
191-
onPress={logEvent('press')}
192-
onPressIn={logEvent('pressIn')}
193-
onPressOut={logEvent('pressOut')}
194-
onLongPress={logEvent('longPress')}
195-
>
196-
press me
197-
</Text>,
198-
);
199-
await userEvent.press(screen.getByText('press me'));
200-
201-
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
202-
});
203-
204229
test('does not trigger on disabled Text', async () => {
205230
const { events, logEvent } = createEventLogger();
206231

@@ -240,22 +265,7 @@ describe('userEvent.press with real timers', () => {
240265
expect(events).toEqual([]);
241266
});
242267

243-
test('works on TetInput', async () => {
244-
const { events, logEvent } = createEventLogger();
245-
246-
render(
247-
<TextInput
248-
placeholder="email"
249-
onPressIn={logEvent('pressIn')}
250-
onPressOut={logEvent('pressOut')}
251-
/>,
252-
);
253-
await userEvent.press(screen.getByPlaceholderText('email'));
254-
255-
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
256-
});
257-
258-
test('does not call onPressIn and onPressOut on non editable TetInput', async () => {
268+
test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
259269
const { events, logEvent } = createEventLogger();
260270

261271
render(
@@ -270,7 +280,7 @@ describe('userEvent.press with real timers', () => {
270280
expect(events).toEqual([]);
271281
});
272282

273-
test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => {
283+
test('does not call onPressIn and onPressOut on TextInput with pointer events disabled', async () => {
274284
const { events, logEvent } = createEventLogger();
275285

276286
render(
@@ -295,7 +305,6 @@ describe('userEvent.press with real timers', () => {
295305
);
296306

297307
await userEvent.press(screen.getByText('press me'));
298-
299308
expect(mockOnPress).toHaveBeenCalled();
300309
});
301310
});

‎src/user-event/press/__tests__/press.test.tsx

+96-86
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('userEvent.press with fake timers', () => {
1818
jest.setSystemTime(0);
1919
});
2020

21-
test('calls onPressIn, onPress and onPressOut prop of touchable', async () => {
21+
test('works on Pressable', async () => {
2222
const { events, logEvent } = createEventLogger();
2323
const user = userEvent.setup();
2424

@@ -31,11 +31,80 @@ describe('userEvent.press with fake timers', () => {
3131
testID="pressable"
3232
/>,
3333
);
34-
await user.press(screen.getByTestId('pressable'));
3534

35+
await user.press(screen.getByTestId('pressable'));
36+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
3637
expect(events).toMatchSnapshot();
3738
});
3839

40+
test('works on TouchableOpacity', async () => {
41+
const mockOnPress = jest.fn();
42+
43+
render(
44+
<TouchableOpacity onPress={mockOnPress}>
45+
<Text>press me</Text>
46+
</TouchableOpacity>,
47+
);
48+
49+
await userEvent.press(screen.getByText('press me'));
50+
expect(mockOnPress).toHaveBeenCalled();
51+
});
52+
53+
test('works on TouchableHighlight', async () => {
54+
const mockOnPress = jest.fn();
55+
56+
render(
57+
<TouchableHighlight onPress={mockOnPress}>
58+
<Text>press me</Text>
59+
</TouchableHighlight>,
60+
);
61+
62+
await userEvent.press(screen.getByText('press me'));
63+
expect(mockOnPress).toHaveBeenCalled();
64+
});
65+
66+
test('works on Text', async () => {
67+
const { events, logEvent } = createEventLogger();
68+
69+
render(
70+
<Text
71+
onPress={logEvent('press')}
72+
onPressIn={logEvent('pressIn')}
73+
onPressOut={logEvent('pressOut')}
74+
onLongPress={logEvent('longPress')}
75+
>
76+
press me
77+
</Text>,
78+
);
79+
80+
await userEvent.press(screen.getByText('press me'));
81+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
82+
});
83+
84+
test('works on TextInput', async () => {
85+
const { events, logEvent } = createEventLogger();
86+
87+
render(
88+
<TextInput
89+
placeholder="email"
90+
onPressIn={logEvent('pressIn')}
91+
onPressOut={logEvent('pressOut')}
92+
/>,
93+
);
94+
95+
await userEvent.press(screen.getByPlaceholderText('email'));
96+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
97+
});
98+
99+
test('works on Button', async () => {
100+
const { events, logEvent } = createEventLogger();
101+
102+
render(<Button title="press me" onPress={logEvent('press')} />);
103+
104+
await userEvent.press(screen.getByText('press me'));
105+
expect(getEventsNames(events)).toEqual(['press']);
106+
});
107+
39108
test('does not trigger event when pressable is disabled', async () => {
40109
const { events, logEvent } = createEventLogger();
41110
const user = userEvent.setup();
@@ -129,7 +198,7 @@ describe('userEvent.press with fake timers', () => {
129198
);
130199
await user.press(screen.getByTestId('pressable'));
131200

132-
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
201+
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
133202
});
134203

135204
test('crawls up in the tree to find an element that responds to touch events', async () => {
@@ -158,59 +227,6 @@ describe('userEvent.press with fake timers', () => {
158227
expect(mockOnLongPress).not.toHaveBeenCalled();
159228
});
160229

161-
test('works on TouchableOpacity', async () => {
162-
const mockOnPress = jest.fn();
163-
164-
render(
165-
<TouchableOpacity onPress={mockOnPress}>
166-
<Text>press me</Text>
167-
</TouchableOpacity>,
168-
);
169-
await userEvent.press(screen.getByText('press me'));
170-
171-
expect(mockOnPress).toHaveBeenCalled();
172-
});
173-
174-
test('works on TouchableHighlight', async () => {
175-
const mockOnPress = jest.fn();
176-
177-
render(
178-
<TouchableHighlight onPress={mockOnPress}>
179-
<Text>press me</Text>
180-
</TouchableHighlight>,
181-
);
182-
await userEvent.press(screen.getByText('press me'));
183-
184-
expect(mockOnPress).toHaveBeenCalled();
185-
});
186-
187-
test('press works on Text', async () => {
188-
const { events, logEvent } = createEventLogger();
189-
190-
render(
191-
<Text
192-
onPress={logEvent('press')}
193-
onPressIn={logEvent('pressIn')}
194-
onPressOut={logEvent('pressOut')}
195-
onLongPress={logEvent('longPress')}
196-
>
197-
press me
198-
</Text>,
199-
);
200-
201-
await userEvent.press(screen.getByText('press me'));
202-
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
203-
});
204-
205-
test('press works on Button', async () => {
206-
const { events, logEvent } = createEventLogger();
207-
208-
render(<Button title="press me" onPress={logEvent('press')} />);
209-
210-
await userEvent.press(screen.getByText('press me'));
211-
expect(getEventsNames(events)).toEqual(['press']);
212-
});
213-
214230
test('longPress works Text', async () => {
215231
const { events, logEvent } = createEventLogger();
216232

@@ -268,36 +284,6 @@ describe('userEvent.press with fake timers', () => {
268284
expect(events).toEqual([]);
269285
});
270286

271-
test('press works on TextInput', async () => {
272-
const { events, logEvent } = createEventLogger();
273-
274-
render(
275-
<TextInput
276-
placeholder="email"
277-
onPressIn={logEvent('pressIn')}
278-
onPressOut={logEvent('pressOut')}
279-
/>,
280-
);
281-
282-
await userEvent.press(screen.getByPlaceholderText('email'));
283-
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
284-
});
285-
286-
test('longPress works on TextInput', async () => {
287-
const { events, logEvent } = createEventLogger();
288-
289-
render(
290-
<TextInput
291-
placeholder="email"
292-
onPressIn={logEvent('pressIn')}
293-
onPressOut={logEvent('pressOut')}
294-
/>,
295-
);
296-
297-
await userEvent.longPress(screen.getByPlaceholderText('email'));
298-
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
299-
});
300-
301287
test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
302288
const { events, logEvent } = createEventLogger();
303289

@@ -372,3 +358,27 @@ describe('userEvent.press with fake timers', () => {
372358
expect(consoleErrorSpy).not.toHaveBeenCalled();
373359
});
374360
});
361+
362+
function Component() {
363+
const [mounted, setMounted] = React.useState(true);
364+
365+
const onPressIn = () => {
366+
setMounted(false);
367+
};
368+
369+
return (
370+
<View>
371+
{mounted && (
372+
<Pressable onPressIn={onPressIn}>
373+
<Text>Unmount</Text>
374+
</Pressable>
375+
)}
376+
</View>
377+
);
378+
}
379+
380+
test('unmounts component', async () => {
381+
render(<Component />);
382+
await userEvent.press(screen.getByText('Unmount'));
383+
expect(screen.queryByText('Unmount')).not.toBeOnTheScreen();
384+
});

‎src/user-event/press/constants.ts

-7
This file was deleted.

‎src/user-event/press/press.ts

+25-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import { isHostText, isHostTextInput } from '../../helpers/host-component-names'
77
import { EventBuilder } from '../event-builder';
88
import { UserEventConfig, UserEventInstance } from '../setup';
99
import { dispatchEvent, wait } from '../utils';
10-
import { DEFAULT_MIN_PRESS_DURATION } from './constants';
10+
11+
// These are constants defined in the React Native repo
12+
// See: https://github.com/facebook/react-native/blob/50e38cc9f1e6713228a91ad50f426c4f65e65e1a/packages/react-native/Libraries/Pressability/Pressability.js#L264
13+
export const DEFAULT_MIN_PRESS_DURATION = 130;
14+
export const DEFAULT_LONG_PRESS_DELAY_MS = 500;
1115

1216
export interface PressOptions {
1317
duration?: number;
@@ -16,7 +20,6 @@ export interface PressOptions {
1620
export async function press(this: UserEventInstance, element: ReactTestInstance): Promise<void> {
1721
await basePress(this.config, element, {
1822
type: 'press',
19-
duration: 0,
2023
});
2124
}
2225

@@ -27,13 +30,13 @@ export async function longPress(
2730
): Promise<void> {
2831
await basePress(this.config, element, {
2932
type: 'longPress',
30-
duration: options?.duration ?? 500,
33+
duration: options?.duration ?? DEFAULT_LONG_PRESS_DELAY_MS,
3134
});
3235
}
3336

3437
interface BasePressOptions {
3538
type: 'press' | 'longPress';
36-
duration: number;
39+
duration?: number;
3740
}
3841

3942
const basePress = async (
@@ -73,16 +76,17 @@ const emitPressablePressEvents = async (
7376

7477
dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant());
7578

76-
await wait(config, options.duration);
79+
const duration = options.duration ?? DEFAULT_MIN_PRESS_DURATION;
80+
await wait(config, duration);
7781

7882
dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease());
7983

8084
// React Native will wait for minimal delay of DEFAULT_MIN_PRESS_DURATION
8185
// before emitting the `pressOut` event. We need to wait here, so that
8286
// `press()` function does not return before that.
83-
if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) {
87+
if (DEFAULT_MIN_PRESS_DURATION - duration > 0) {
8488
await act(async () => {
85-
await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration);
89+
await wait(config, DEFAULT_MIN_PRESS_DURATION - duration);
8690
});
8791
}
8892
};
@@ -118,11 +122,22 @@ async function emitTextPressEvents(
118122
await wait(config);
119123
dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
120124

121-
// Emit either `press` or `longPress`.
122-
dispatchEvent(element, options.type, EventBuilder.Common.touch());
123-
124125
await wait(config, options.duration);
126+
127+
// Long press events are emitted before `pressOut`.
128+
if (options.type === 'longPress') {
129+
dispatchEvent(element, 'longPress', EventBuilder.Common.touch());
130+
}
131+
125132
dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
133+
134+
// Regular press events are emitted after `pressOut` according to the React Native docs.
135+
// See: https://reactnative.dev/docs/pressable#onpress
136+
// Experimentally for very short presses (< 130ms) `press` events are actually emitted before `onPressOut`, but
137+
// we will ignore that as in reality most pressed would be above the 130ms threshold.
138+
if (options.type === 'press') {
139+
dispatchEvent(element, 'press', EventBuilder.Common.touch());
140+
}
126141
}
127142

128143
/**

‎src/user-event/utils/dispatch-event.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import act from '../../act';
3+
import { isElementMounted } from '../../helpers/component-tree';
34

45
/**
56
* Basic dispatch event function used by User Event module.
@@ -9,6 +10,10 @@ import act from '../../act';
910
* @param event event payload(s)
1011
*/
1112
export function dispatchEvent(element: ReactTestInstance, eventName: string, ...event: unknown[]) {
13+
if (!isElementMounted(element)) {
14+
return;
15+
}
16+
1217
const handler = getEventHandler(element, eventName);
1318
if (!handler) {
1419
return;

0 commit comments

Comments
 (0)
Please sign in to comment.