diff --git a/experiments-app/src/experiments.ts b/experiments-app/src/experiments.ts
index fdd485148..af889cfb1 100644
--- a/experiments-app/src/experiments.ts
+++ b/experiments-app/src/experiments.ts
@@ -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';
@@ -13,6 +14,11 @@ export const experiments = [
title: 'Accessibility',
component: AccessibilityScreen,
},
+ {
+ key: 'PressEvents',
+ title: 'Press Events',
+ component: PressEvents,
+ },
{
key: 'TextInputEvents',
title: 'TextInput Events',
diff --git a/experiments-app/src/screens/PressEvents.tsx b/experiments-app/src/screens/PressEvents.tsx
new file mode 100644
index 000000000..a8ba3edcc
--- /dev/null
+++ b/experiments-app/src/screens/PressEvents.tsx
@@ -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 (
+
+
+
+
+
+
+ Text
+
+
+
+
+ Pressable
+
+
+
+
+ Pressable
+
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/experiments-app/src/utils/helpers.ts b/experiments-app/src/utils/helpers.ts
index 5993a46c4..1366177b7 100644
--- a/experiments-app/src/utils/helpers.ts
+++ b/experiments-app/src/utils/helpers.ts
@@ -1,5 +1,7 @@
import { NativeSyntheticEvent } from 'react-native/types';
+let lastEventTimeStamp: number | null = null;
+
export function nativeEventLogger(name: string) {
return (event: NativeSyntheticEvent) => {
logEvent(name, event?.nativeEvent);
@@ -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();
}
diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx
index 3127963d7..fae79012b 100644
--- a/src/__tests__/render.test.tsx
+++ b/src/__tests__/render.test.tsx
@@ -249,5 +249,5 @@ test('supports legacy rendering', () => {
test('supports concurrent rendering', () => {
render(, { concurrentRoot: true });
- expect(screen.root).toBeDefined();
+ expect(screen.root).toBeOnTheScreen();
});
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 98c7745a3..0f0287f5e 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -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';
@@ -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);
diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts
index 8387278b5..4a4a00897 100644
--- a/src/helpers/component-tree.ts
+++ b/src/helpers/component-tree.ts
@@ -1,5 +1,5 @@
import { ReactTestInstance } from 'react-test-renderer';
-
+import { screen } from '../screen';
/**
* ReactTestInstance referring to host element.
*/
@@ -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.
diff --git a/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap
index fa31b7a34..b1c8a3fb2 100644
--- a/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap
+++ b/src/user-event/press/__tests__/__snapshots__/longPress.test.tsx.snap
@@ -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,
+ },
+ },
+]
+`;
diff --git a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap
index ceb2803f3..d4dc3df36 100644
--- a/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap
+++ b/src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap
@@ -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",
@@ -33,7 +33,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
},
},
{
- "name": "press",
+ "name": "pressOut",
"payload": {
"currentTarget": {
"measure": [Function],
@@ -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],
@@ -63,7 +63,7 @@ exports[`userEvent.press with fake timers calls onPressIn, onPress and onPressOu
},
},
{
- "name": "pressOut",
+ "name": "press",
"payload": {
"currentTarget": {
"measure": [Function],
@@ -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],
diff --git a/src/user-event/press/__tests__/longPress.real-timers.test.tsx b/src/user-event/press/__tests__/longPress.real-timers.test.tsx
index 928e5b6d3..4c693830c 100644
--- a/src/user-event/press/__tests__/longPress.real-timers.test.tsx
+++ b/src/user-event/press/__tests__/longPress.real-timers.test.tsx
@@ -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', () => {
@@ -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(
+ ,
+ );
+
+ await user.longPress(screen.getByTestId('pressable'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
+ });
+
+ test('works on TouchableOpacity', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.longPress(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on TouchableHighlight', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.longPress(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on Text', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+
+ press me
+ ,
+ );
+
+ 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();
diff --git a/src/user-event/press/__tests__/longPress.test.tsx b/src/user-event/press/__tests__/longPress.test.tsx
index 3716446e7..bbf5d0582 100644
--- a/src/user-event/press/__tests__/longPress.test.tsx
+++ b/src/user-event/press/__tests__/longPress.test.tsx
@@ -1,8 +1,8 @@
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 '../..';
-import { createEventLogger } from '../../../test-utils';
describe('userEvent.longPress with fake timers', () => {
beforeEach(() => {
@@ -10,6 +10,69 @@ describe('userEvent.longPress with fake timers', () => {
jest.setSystemTime(0);
});
+ test('works on Pressable', async () => {
+ const { events, logEvent } = createEventLogger();
+ const user = userEvent.setup();
+
+ render(
+ ,
+ );
+
+ await user.longPress(screen.getByTestId('pressable'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
+ expect(events).toMatchSnapshot();
+ });
+
+ test('works on TouchableOpacity', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.longPress(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on TouchableHighlight', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.longPress(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on Text', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+
+ press me
+ ,
+ );
+
+ 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 { logEvent, events } = createEventLogger();
const user = userEvent.setup();
diff --git a/src/user-event/press/__tests__/press.real-timers.test.tsx b/src/user-event/press/__tests__/press.real-timers.test.tsx
index 1ba53c27e..042b9eded 100644
--- a/src/user-event/press/__tests__/press.real-timers.test.tsx
+++ b/src/user-event/press/__tests__/press.real-timers.test.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import {
+ Button,
Pressable,
Text,
TextInput,
@@ -17,7 +18,7 @@ describe('userEvent.press with real timers', () => {
jest.restoreAllMocks();
});
- test('calls onPressIn, onPress and onPressOut prop of touchable', async () => {
+ test('works on Pressable', async () => {
const { events, logEvent } = createEventLogger();
const user = userEvent.setup();
@@ -30,9 +31,77 @@ describe('userEvent.press with real timers', () => {
testID="pressable"
/>,
);
+
await user.press(screen.getByTestId('pressable'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
+ });
+
+ test('works on TouchableOpacity', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
- expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
+ await userEvent.press(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on TouchableHighlight', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on Text', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
+ });
+
+ test('works on TextInput', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+ ,
+ );
+
+ await userEvent.press(screen.getByPlaceholderText('email'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
+ });
+
+ test('works on Button', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render();
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(getEventsNames(events)).toEqual(['press']);
});
test('does not trigger event when pressable is disabled', async () => {
@@ -128,7 +197,7 @@ describe('userEvent.press with real timers', () => {
);
await user.press(screen.getByTestId('pressable'));
- expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
});
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', () => {
expect(mockOnLongPress).not.toHaveBeenCalled();
});
- test('works on TouchableOpacity', async () => {
- const mockOnPress = jest.fn();
-
- render(
-
- press me
- ,
- );
- await userEvent.press(screen.getByText('press me'));
-
- expect(mockOnPress).toHaveBeenCalled();
- });
-
- test('works on TouchableHighlight', async () => {
- const mockOnPress = jest.fn();
-
- render(
-
- press me
- ,
- );
- await userEvent.press(screen.getByText('press me'));
-
- expect(mockOnPress).toHaveBeenCalled();
- });
-
- test('works on Text', async () => {
- const { events, logEvent } = createEventLogger();
-
- render(
-
- press me
- ,
- );
- await userEvent.press(screen.getByText('press me'));
-
- expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
- });
-
test('does not trigger on disabled Text', async () => {
const { events, logEvent } = createEventLogger();
@@ -240,22 +265,7 @@ describe('userEvent.press with real timers', () => {
expect(events).toEqual([]);
});
- test('works on TetInput', async () => {
- const { events, logEvent } = createEventLogger();
-
- render(
- ,
- );
- await userEvent.press(screen.getByPlaceholderText('email'));
-
- expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
- });
-
- test('does not call onPressIn and onPressOut on non editable TetInput', async () => {
+ test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
const { events, logEvent } = createEventLogger();
render(
@@ -270,7 +280,7 @@ describe('userEvent.press with real timers', () => {
expect(events).toEqual([]);
});
- test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => {
+ test('does not call onPressIn and onPressOut on TextInput with pointer events disabled', async () => {
const { events, logEvent } = createEventLogger();
render(
@@ -295,7 +305,6 @@ describe('userEvent.press with real timers', () => {
);
await userEvent.press(screen.getByText('press me'));
-
expect(mockOnPress).toHaveBeenCalled();
});
});
diff --git a/src/user-event/press/__tests__/press.test.tsx b/src/user-event/press/__tests__/press.test.tsx
index c4ff8be74..a15ac98ed 100644
--- a/src/user-event/press/__tests__/press.test.tsx
+++ b/src/user-event/press/__tests__/press.test.tsx
@@ -18,7 +18,7 @@ describe('userEvent.press with fake timers', () => {
jest.setSystemTime(0);
});
- test('calls onPressIn, onPress and onPressOut prop of touchable', async () => {
+ test('works on Pressable', async () => {
const { events, logEvent } = createEventLogger();
const user = userEvent.setup();
@@ -31,11 +31,80 @@ describe('userEvent.press with fake timers', () => {
testID="pressable"
/>,
);
- await user.press(screen.getByTestId('pressable'));
+ await user.press(screen.getByTestId('pressable'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
expect(events).toMatchSnapshot();
});
+ test('works on TouchableOpacity', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on TouchableHighlight', async () => {
+ const mockOnPress = jest.fn();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(mockOnPress).toHaveBeenCalled();
+ });
+
+ test('works on Text', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+
+ press me
+ ,
+ );
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
+ });
+
+ test('works on TextInput', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render(
+ ,
+ );
+
+ await userEvent.press(screen.getByPlaceholderText('email'));
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
+ });
+
+ test('works on Button', async () => {
+ const { events, logEvent } = createEventLogger();
+
+ render();
+
+ await userEvent.press(screen.getByText('press me'));
+ expect(getEventsNames(events)).toEqual(['press']);
+ });
+
test('does not trigger event when pressable is disabled', async () => {
const { events, logEvent } = createEventLogger();
const user = userEvent.setup();
@@ -129,7 +198,7 @@ describe('userEvent.press with fake timers', () => {
);
await user.press(screen.getByTestId('pressable'));
- expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
+ expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
});
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', () => {
expect(mockOnLongPress).not.toHaveBeenCalled();
});
- test('works on TouchableOpacity', async () => {
- const mockOnPress = jest.fn();
-
- render(
-
- press me
- ,
- );
- await userEvent.press(screen.getByText('press me'));
-
- expect(mockOnPress).toHaveBeenCalled();
- });
-
- test('works on TouchableHighlight', async () => {
- const mockOnPress = jest.fn();
-
- render(
-
- press me
- ,
- );
- await userEvent.press(screen.getByText('press me'));
-
- expect(mockOnPress).toHaveBeenCalled();
- });
-
- test('press works on Text', async () => {
- const { events, logEvent } = createEventLogger();
-
- render(
-
- press me
- ,
- );
-
- await userEvent.press(screen.getByText('press me'));
- expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
- });
-
- test('press works on Button', async () => {
- const { events, logEvent } = createEventLogger();
-
- render();
-
- await userEvent.press(screen.getByText('press me'));
- expect(getEventsNames(events)).toEqual(['press']);
- });
-
test('longPress works Text', async () => {
const { events, logEvent } = createEventLogger();
@@ -268,36 +284,6 @@ describe('userEvent.press with fake timers', () => {
expect(events).toEqual([]);
});
- test('press works on TextInput', async () => {
- const { events, logEvent } = createEventLogger();
-
- render(
- ,
- );
-
- await userEvent.press(screen.getByPlaceholderText('email'));
- expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
- });
-
- test('longPress works on TextInput', async () => {
- const { events, logEvent } = createEventLogger();
-
- render(
- ,
- );
-
- await userEvent.longPress(screen.getByPlaceholderText('email'));
- expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
- });
-
test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
const { events, logEvent } = createEventLogger();
@@ -372,3 +358,27 @@ describe('userEvent.press with fake timers', () => {
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
+
+function Component() {
+ const [mounted, setMounted] = React.useState(true);
+
+ const onPressIn = () => {
+ setMounted(false);
+ };
+
+ return (
+
+ {mounted && (
+
+ Unmount
+
+ )}
+
+ );
+}
+
+test('unmounts component', async () => {
+ render();
+ await userEvent.press(screen.getByText('Unmount'));
+ expect(screen.queryByText('Unmount')).not.toBeOnTheScreen();
+});
diff --git a/src/user-event/press/constants.ts b/src/user-event/press/constants.ts
deleted file mode 100644
index 8d237a0db..000000000
--- a/src/user-event/press/constants.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// These are constants defined in the React Native repo
-
-// Used to define the delay before calling onPressOut after a press
-export const DEFAULT_MIN_PRESS_DURATION = 130;
-
-// Default minimum press duration to trigger a long press
-export const DEFAULT_LONG_PRESS_DELAY_MS = 500;
diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts
index c49ea7090..7302c704a 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -7,7 +7,11 @@ import { isHostText, isHostTextInput } from '../../helpers/host-component-names'
import { EventBuilder } from '../event-builder';
import { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, wait } from '../utils';
-import { DEFAULT_MIN_PRESS_DURATION } from './constants';
+
+// These are constants defined in the React Native repo
+// See: https://github.com/facebook/react-native/blob/50e38cc9f1e6713228a91ad50f426c4f65e65e1a/packages/react-native/Libraries/Pressability/Pressability.js#L264
+export const DEFAULT_MIN_PRESS_DURATION = 130;
+export const DEFAULT_LONG_PRESS_DELAY_MS = 500;
export interface PressOptions {
duration?: number;
@@ -16,7 +20,6 @@ export interface PressOptions {
export async function press(this: UserEventInstance, element: ReactTestInstance): Promise {
await basePress(this.config, element, {
type: 'press',
- duration: 0,
});
}
@@ -27,13 +30,13 @@ export async function longPress(
): Promise {
await basePress(this.config, element, {
type: 'longPress',
- duration: options?.duration ?? 500,
+ duration: options?.duration ?? DEFAULT_LONG_PRESS_DELAY_MS,
});
}
interface BasePressOptions {
type: 'press' | 'longPress';
- duration: number;
+ duration?: number;
}
const basePress = async (
@@ -73,16 +76,17 @@ const emitPressablePressEvents = async (
dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant());
- await wait(config, options.duration);
+ const duration = options.duration ?? DEFAULT_MIN_PRESS_DURATION;
+ await wait(config, duration);
dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease());
// React Native will wait for minimal delay of DEFAULT_MIN_PRESS_DURATION
// before emitting the `pressOut` event. We need to wait here, so that
// `press()` function does not return before that.
- if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) {
+ if (DEFAULT_MIN_PRESS_DURATION - duration > 0) {
await act(async () => {
- await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration);
+ await wait(config, DEFAULT_MIN_PRESS_DURATION - duration);
});
}
};
@@ -118,11 +122,22 @@ async function emitTextPressEvents(
await wait(config);
dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
- // Emit either `press` or `longPress`.
- dispatchEvent(element, options.type, EventBuilder.Common.touch());
-
await wait(config, options.duration);
+
+ // Long press events are emitted before `pressOut`.
+ if (options.type === 'longPress') {
+ dispatchEvent(element, 'longPress', EventBuilder.Common.touch());
+ }
+
dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
+
+ // Regular press events are emitted after `pressOut` according to the React Native docs.
+ // See: https://reactnative.dev/docs/pressable#onpress
+ // Experimentally for very short presses (< 130ms) `press` events are actually emitted before `onPressOut`, but
+ // we will ignore that as in reality most pressed would be above the 130ms threshold.
+ if (options.type === 'press') {
+ dispatchEvent(element, 'press', EventBuilder.Common.touch());
+ }
}
/**
diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts
index 6d284f633..d1202fd54 100644
--- a/src/user-event/utils/dispatch-event.ts
+++ b/src/user-event/utils/dispatch-event.ts
@@ -1,5 +1,6 @@
import { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
+import { isElementMounted } from '../../helpers/component-tree';
/**
* Basic dispatch event function used by User Event module.
@@ -9,6 +10,10 @@ import act from '../../act';
* @param event event payload(s)
*/
export function dispatchEvent(element: ReactTestInstance, eventName: string, ...event: unknown[]) {
+ if (!isElementMounted(element)) {
+ return;
+ }
+
const handler = getEventHandler(element, eventName);
if (!handler) {
return;