Skip to content

Commit

Permalink
fix: press event order (#1696)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjastrzebski authored Oct 30, 2024
1 parent 0632000 commit 5de8790
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 182 deletions.
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

0 comments on commit 5de8790

Please sign in to comment.