Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: press event order #1696

Merged
merged 11 commits into from
Oct 30, 2024
6 changes: 6 additions & 0 deletions experiments-app/src/experiments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccessibilityScreen } from './screens/Accessibility';
import { PressEvents } from './screens/PressEvents';
import { TextInputEventPropagation } from './screens/TextInputEventPropagation';
import { TextInputEvents } from './screens/TextInputEvents';
import { ScrollViewEvents } from './screens/ScrollViewEvents';
Expand All @@ -13,6 +14,11 @@ export const experiments = [
title: 'Accessibility',
component: AccessibilityScreen,
},
{
key: 'PressEvents',
title: 'Press Events',
component: PressEvents,
},
{
key: 'TextInputEvents',
title: 'TextInput Events',
Expand Down
82 changes: 82 additions & 0 deletions experiments-app/src/screens/PressEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import {
StyleSheet,
SafeAreaView,
Text,
TextInput,
View,
Pressable,
TouchableOpacity,
} from 'react-native';
import { nativeEventLogger, logEvent } from '../utils/helpers';

export function PressEvents() {
const [value, setValue] = React.useState('');

const handleChangeText = (value: string) => {
setValue(value);
logEvent('changeText', value);
};

return (
<SafeAreaView style={styles.container}>
<View style={styles.wrapper}>
<TextInput
style={styles.textInput}
value={value}
onPress={nativeEventLogger('press')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
/>
</View>
<View style={styles.wrapper}>
<Text
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
Text
</Text>
</View>
<View style={styles.wrapper}>
<Pressable
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
<Text>Pressable</Text>
</Pressable>
</View>
<View style={styles.wrapper}>
<TouchableOpacity
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
<Text>Pressable</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
wrapper: {
padding: 20,
backgroundColor: 'yellow',
},
textInput: {
backgroundColor: 'white',
margin: 20,
padding: 8,
fontSize: 18,
borderWidth: 1,
borderColor: 'grey',
},
});
5 changes: 4 additions & 1 deletion experiments-app/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NativeSyntheticEvent } from 'react-native/types';

let lastEventTimeStamp: number | null = null;

export function nativeEventLogger(name: string) {
return (event: NativeSyntheticEvent<unknown>) => {
logEvent(name, event?.nativeEvent);
Expand All @@ -14,5 +16,6 @@ export function customEventLogger(name: string) {

export function logEvent(name: string, ...args: unknown[]) {
// eslint-disable-next-line no-console
console.log(`Event: ${name}`, ...args);
console.log(`[${Date.now() - (lastEventTimeStamp ?? Date.now())}ms] Event: ${name}`, ...args);
lastEventTimeStamp = Date.now();
}
2 changes: 1 addition & 1 deletion src/__tests__/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,5 @@ test('supports legacy rendering', () => {

test('supports concurrent rendering', () => {
render(<View testID="test" />, { concurrentRoot: true });
expect(screen.root).toBeDefined();
expect(screen.root).toBeOnTheScreen();
});
6 changes: 5 additions & 1 deletion src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ScrollViewProps,
} from 'react-native';
import act from './act';
import { isHostElement } from './helpers/component-tree';
import { isElementMounted, isHostElement } from './helpers/component-tree';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isTextInputEditable } from './helpers/text-input';
Expand Down Expand Up @@ -121,6 +121,10 @@ type EventName = StringWithAutocomplete<
>;

function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) {
if (!isElementMounted(element)) {
return;
}

setNativeStateIfNeeded(element, eventName, data[0]);

const handler = findEventHandler(element, eventName);
Expand Down
6 changes: 5 additions & 1 deletion src/helpers/component-tree.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactTestInstance } from 'react-test-renderer';

import { screen } from '../screen';
/**
* ReactTestInstance referring to host element.
*/
Expand All @@ -13,6 +13,10 @@ export function isHostElement(element?: ReactTestInstance | null): element is Ho
return typeof element?.type === 'string';
}

export function isElementMounted(element: ReactTestInstance | null) {
return getUnsafeRootElement(element) === screen.UNSAFE_root;
}

/**
* Returns first host ancestor for given element.
* @param element The element start traversing from.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,98 @@ exports[`userEvent.longPress with fake timers calls onLongPress if the delayLong
},
]
`;

exports[`userEvent.longPress with fake timers works on Pressable 1`] = `
[
{
"name": "pressIn",
"payload": {
"currentTarget": {
"measure": [Function],
},
"dispatchConfig": {
"registrationName": "onResponderGrant",
},
"isDefaultPrevented": [Function],
"isPersistent": [Function],
"isPropagationStopped": [Function],
"nativeEvent": {
"changedTouches": [],
"identifier": 0,
"locationX": 0,
"locationY": 0,
"pageX": 0,
"pageY": 0,
"target": 0,
"timestamp": 0,
"touches": [],
},
"persist": [Function],
"preventDefault": [Function],
"stopPropagation": [Function],
"target": {},
"timeStamp": 0,
},
},
{
"name": "longPress",
"payload": {
"currentTarget": {
"measure": [Function],
},
"dispatchConfig": {
"registrationName": "onResponderGrant",
},
"isDefaultPrevented": [Function],
"isPersistent": [Function],
"isPropagationStopped": [Function],
"nativeEvent": {
"changedTouches": [],
"identifier": 0,
"locationX": 0,
"locationY": 0,
"pageX": 0,
"pageY": 0,
"target": 0,
"timestamp": 0,
"touches": [],
},
"persist": [Function],
"preventDefault": [Function],
"stopPropagation": [Function],
"target": {},
"timeStamp": 0,
},
},
{
"name": "pressOut",
"payload": {
"currentTarget": {
"measure": [Function],
},
"dispatchConfig": {
"registrationName": "onResponderRelease",
},
"isDefaultPrevented": [Function],
"isPersistent": [Function],
"isPropagationStopped": [Function],
"nativeEvent": {
"changedTouches": [],
"identifier": 0,
"locationX": 0,
"locationY": 0,
"pageX": 0,
"pageY": 0,
"target": 0,
"timestamp": 500,
"touches": [],
},
"persist": [Function],
"preventDefault": [Function],
"stopPropagation": [Function],
"target": {},
"timeStamp": 0,
},
},
]
`;
10 changes: 5 additions & 5 deletions src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOut prop of touchable 1`] = `
exports[`userEvent.press with fake timers works on Pressable 1`] = `
[
{
"name": "pressIn",
Expand Down Expand Up @@ -33,7 +33,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
},
},
{
"name": "press",
"name": "pressOut",
"payload": {
"currentTarget": {
"measure": [Function],
Expand All @@ -52,7 +52,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
"pageX": 0,
"pageY": 0,
"target": 0,
"timestamp": 0,
"timestamp": 130,
"touches": [],
},
"persist": [Function],
Expand All @@ -63,7 +63,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
},
},
{
"name": "pressOut",
"name": "press",
"payload": {
"currentTarget": {
"measure": [Function],
Expand All @@ -82,7 +82,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
"pageX": 0,
"pageY": 0,
"target": 0,
"timestamp": 0,
"timestamp": 130,
"touches": [],
},
"persist": [Function],
Expand Down
67 changes: 65 additions & 2 deletions src/user-event/press/__tests__/longPress.real-timers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { Pressable, Text } from 'react-native';
import { render, screen } from '../../../pure';
import { Pressable, Text, TouchableHighlight, TouchableOpacity } from 'react-native';
import { createEventLogger, getEventsNames } from '../../../test-utils';
import { render, screen } from '../../..';
import { userEvent } from '../..';

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

test('works on Pressable', 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'));
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
});

test('works on TouchableOpacity', async () => {
const mockOnPress = jest.fn();

render(
<TouchableOpacity onPress={mockOnPress}>
<Text>press me</Text>
</TouchableOpacity>,
);

await userEvent.longPress(screen.getByText('press me'));
expect(mockOnPress).toHaveBeenCalled();
});

test('works on TouchableHighlight', async () => {
const mockOnPress = jest.fn();

render(
<TouchableHighlight onPress={mockOnPress}>
<Text>press me</Text>
</TouchableHighlight>,
);

await userEvent.longPress(screen.getByText('press me'));
expect(mockOnPress).toHaveBeenCalled();
});

test('works on Text', async () => {
const { events, logEvent } = createEventLogger();

render(
<Text
onPress={logEvent('press')}
onPressIn={logEvent('pressIn')}
onPressOut={logEvent('pressOut')}
onLongPress={logEvent('longPress')}
>
press me
</Text>,
);

await userEvent.longPress(screen.getByText('press me'));
expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
});

test('calls onLongPress if the delayLongPress is the default one', async () => {
const mockOnLongPress = jest.fn();
const user = userEvent.setup();
Expand Down
Loading
Loading