Skip to content

Commit 5de8790

Browse files
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();

0 commit comments

Comments
 (0)