diff --git a/.gemini/settings.json b/.gemini/settings.json
new file mode 100644
index 000000000..2facafdd1
--- /dev/null
+++ b/.gemini/settings.json
@@ -0,0 +1,5 @@
+{
+ "context": {
+ "contextFileName": "AGENTS.md"
+ }
+}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..34c265f26
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,48 @@
+# Code Assistant Context
+
+This document provides context for the any code assistant to understand the `@testing-library/react-native` project.
+
+## Project Overview
+
+`@testing-library/react-native` (RNTL) provides a set of utilities for testing React Native components. It is designed to facilitate writing tests that resemble how users interact with the application, avoiding implementation details.
+
+- **Core Principle:** "The more your tests resemble the way your software is used, the more confidence they can give you."
+- **Tech Stack:** TypeScript, React Native, Jest.
+- **Architecture:** The library simulates the React Native runtime on top of `universal-test-renderer`.
+
+## Building and Running
+
+The project uses `yarn` for dependency management and script execution.
+
+- **Installation:** `yarn install`
+- **Run Tests:** `yarn test` (Runs Jest)
+- **Run Tests (CI):** `yarn test:ci` (Runs Jest with worker limits)
+- **Lint Code:** `yarn lint` (Runs ESLint on `src`)
+- **Type Check:** `yarn typecheck` (Runs TypeScript compiler)
+- **Format Check:** `yarn prettier`
+- **Validate All:** `yarn validate` (Runs Prettier, ESLint, Typecheck, and Tests in sequence)
+- **Build Project:** `yarn build` (Cleans, builds JS with Babel, builds TS types, and copies Flow types)
+
+## Development Conventions
+
+- **Code Style:**
+ - **Linting:** ESLint is configured with `@callstack/eslint-config` and `typescript-eslint`. It enforces strict rules, including `no-console` and consistent type imports.
+ - **Formatting:** Prettier is used for code formatting (single quotes, trailing commas).
+ - **Imports:** Sorted using `eslint-plugin-simple-import-sort`.
+
+- **Testing:**
+ - **Framework:** Jest with `react-native` preset.
+ - **Location:** Tests are located within `src`, typically co-located in `__tests__` directories.
+ - **Setup:** `jest-setup.ts` configures the test environment. `src/index.ts` automatically configures cleanup after each test unless skipped.
+ - **Coverage:** Collected from `src`, excluding tests.
+
+- **Commits & Releases:**
+ - **Commits:** Follow the **Conventional Commits** specification (e.g., `fix:`, `feat:`, `chore:`). This is enforced and used for changelog generation.
+ - **Releases:** Managed via `release-it`.
+
+- **File Structure:**
+ - `src/`: Source code.
+ - `src/pure.ts`: Core logic without side effects (no auto-cleanup).
+ - `src/index.ts`: Main entry point, re-exports `pure` and adds side effects (auto-cleanup).
+ - `examples/`: Example React Native applications using the library.
+ - `website/`: Documentation website.
diff --git a/src/__tests__/act.test.tsx b/src/__tests__/act.test.tsx
index f3b373df4..036b80a14 100644
--- a/src/__tests__/act.test.tsx
+++ b/src/__tests__/act.test.tsx
@@ -31,12 +31,12 @@ test('rerender should trigger useEffect', () => {
expect(effectCallback).toHaveBeenCalledTimes(2);
});
-test('fireEvent should trigger useState', () => {
+test('fireEvent should trigger useState', async () => {
render();
const counter = screen.getByText(/Total count/i);
expect(counter.props.children).toEqual('Total count: 0');
- fireEvent.press(counter);
+ await fireEvent.press(counter);
expect(counter.props.children).toEqual('Total count: 1');
});
diff --git a/src/__tests__/deprecated/fire-event-sync.test.tsx b/src/__tests__/deprecated/fire-event-sync.test.tsx
new file mode 100644
index 000000000..d87f09c7f
--- /dev/null
+++ b/src/__tests__/deprecated/fire-event-sync.test.tsx
@@ -0,0 +1,571 @@
+import * as React from 'react';
+import {
+ PanResponder,
+ Pressable,
+ ScrollView,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+
+import { deprecated_fireEventSync, render, screen } from '../..';
+
+type OnPressComponentProps = {
+ onPress: () => void;
+ text: string;
+};
+const OnPressComponent = ({ onPress, text }: OnPressComponentProps) => (
+
+
+ {text}
+
+
+);
+
+type CustomEventComponentProps = {
+ onCustomEvent: () => void;
+};
+const CustomEventComponent = ({ onCustomEvent }: CustomEventComponentProps) => (
+
+ Custom event component
+
+);
+
+type MyCustomButtonProps = {
+ handlePress: () => void;
+ text: string;
+};
+const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => (
+
+);
+
+type CustomEventComponentWithCustomNameProps = {
+ handlePress: () => void;
+};
+const CustomEventComponentWithCustomName = ({
+ handlePress,
+}: CustomEventComponentWithCustomNameProps) => (
+
+);
+
+describe('deprecated_fireEventSync', () => {
+ test('should invoke specified event', () => {
+ const onPressMock = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByText('Press me'), 'press');
+
+ expect(onPressMock).toHaveBeenCalled();
+ });
+
+ test('should invoke specified event on parent element', () => {
+ const onPressMock = jest.fn();
+ const text = 'New press text';
+ render();
+
+ deprecated_fireEventSync(screen.getByText(text), 'press');
+ expect(onPressMock).toHaveBeenCalled();
+ });
+
+ test('should invoke event with custom name', () => {
+ const handlerMock = jest.fn();
+ const EVENT_DATA = 'event data';
+
+ render(
+
+
+ ,
+ );
+
+ deprecated_fireEventSync(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA);
+
+ expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA);
+ });
+});
+
+test('deprecated_fireEventSync.press', () => {
+ const onPressMock = jest.fn();
+ const text = 'Fireevent press';
+ const eventData = {
+ nativeEvent: {
+ pageX: 20,
+ pageY: 30,
+ },
+ };
+ render();
+
+ deprecated_fireEventSync.press(screen.getByText(text), eventData);
+
+ expect(onPressMock).toHaveBeenCalledWith(eventData);
+});
+
+test('deprecated_fireEventSync.scroll', () => {
+ const onScrollMock = jest.fn();
+ const eventData = {
+ nativeEvent: {
+ contentOffset: {
+ y: 200,
+ },
+ },
+ };
+
+ render(
+
+ XD
+ ,
+ );
+
+ deprecated_fireEventSync.scroll(screen.getByText('XD'), eventData);
+
+ expect(onScrollMock).toHaveBeenCalledWith(eventData);
+});
+
+test('deprecated_fireEventSync.changeText', () => {
+ const onChangeTextMock = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ const input = screen.getByPlaceholderText('Customer placeholder');
+ deprecated_fireEventSync.changeText(input, 'content');
+ expect(onChangeTextMock).toHaveBeenCalledWith('content');
+});
+
+it('sets native state value for unmanaged text inputs', () => {
+ render();
+
+ const input = screen.getByTestId('input');
+ expect(input).toHaveDisplayValue('');
+
+ deprecated_fireEventSync.changeText(input, 'abc');
+ expect(input).toHaveDisplayValue('abc');
+});
+
+test('custom component with custom event name', () => {
+ const handlePress = jest.fn();
+
+ render();
+
+ deprecated_fireEventSync(screen.getByText('Custom component'), 'handlePress');
+
+ expect(handlePress).toHaveBeenCalled();
+});
+
+test('event with multiple handler parameters', () => {
+ const handlePress = jest.fn();
+
+ render();
+
+ deprecated_fireEventSync(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2');
+
+ expect(handlePress).toHaveBeenCalledWith('param1', 'param2');
+});
+
+test('should not fire on disabled TouchableOpacity', () => {
+ const handlePress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ expect(handlePress).not.toHaveBeenCalled();
+});
+
+test('should not fire on disabled Pressable', () => {
+ const handlePress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ expect(handlePress).not.toHaveBeenCalled();
+});
+
+test('should not fire inside View with pointerEvents="none" in props', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should not fire inside View with pointerEvents="none" in styles', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should not fire inside View with pointerEvents="none" in styles array', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should not fire inside View with pointerEvents="box-only" in props', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should not fire inside View with pointerEvents="box-only" in styles', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should fire inside View with pointerEvents="box-none" in props', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).toHaveBeenCalledTimes(2);
+});
+
+test('should fire inside View with pointerEvents="box-none" in styles', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).toHaveBeenCalledTimes(2);
+});
+
+test('should fire inside View with pointerEvents="auto" in props', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).toHaveBeenCalledTimes(2);
+});
+
+test('should fire inside View with pointerEvents="auto" in styles', () => {
+ const onPress = jest.fn();
+ render(
+
+
+ Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).toHaveBeenCalledTimes(2);
+});
+
+test('should not fire deeply inside View with pointerEvents="box-only" in props', () => {
+ const onPress = jest.fn();
+ render(
+
+
+
+ Trigger
+
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should not fire deeply inside View with pointerEvents="box-only" in styles', () => {
+ const onPress = jest.fn();
+ render(
+
+
+
+ Trigger
+
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'onPress');
+ expect(onPress).not.toHaveBeenCalled();
+});
+
+test('should fire non-pointer events inside View with pointerEvents="box-none" in props', () => {
+ const onTouchStart = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('view'), 'touchStart');
+ expect(onTouchStart).toHaveBeenCalled();
+});
+
+test('should fire non-pointer events inside View with pointerEvents="box-none" in styles', () => {
+ const onTouchStart = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('view'), 'touchStart');
+ expect(onTouchStart).toHaveBeenCalled();
+});
+
+test('should fire non-touch events inside View with pointerEvents="box-none" in props', () => {
+ const onLayout = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('view'), 'layout');
+ expect(onLayout).toHaveBeenCalled();
+});
+
+test('should fire non-touch events inside View with pointerEvents="box-none" in styles', () => {
+ const onLayout = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('view'), 'layout');
+ expect(onLayout).toHaveBeenCalled();
+});
+
+// This test if pointerEvents="box-only" on composite `Pressable` is blocking
+// the 'press' event on host View rendered by pressable.
+test('should fire on Pressable with pointerEvents="box-only" in props', () => {
+ const onPress = jest.fn();
+ render();
+
+ deprecated_fireEventSync.press(screen.getByTestId('pressable'));
+ expect(onPress).toHaveBeenCalled();
+});
+
+test('should fire on Pressable with pointerEvents="box-only" in styles', () => {
+ const onPress = jest.fn();
+ render();
+
+ deprecated_fireEventSync.press(screen.getByTestId('pressable'));
+ expect(onPress).toHaveBeenCalled();
+});
+
+test('should pass event up on disabled TouchableOpacity', () => {
+ const handleInnerPress = jest.fn();
+ const handleOuterPress = jest.fn();
+ render(
+
+
+ Inner Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Inner Trigger'));
+ expect(handleInnerPress).not.toHaveBeenCalled();
+ expect(handleOuterPress).toHaveBeenCalledTimes(1);
+});
+
+test('should pass event up on disabled Pressable', () => {
+ const handleInnerPress = jest.fn();
+ const handleOuterPress = jest.fn();
+ render(
+
+
+ Inner Trigger
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Inner Trigger'));
+ expect(handleInnerPress).not.toHaveBeenCalled();
+ expect(handleOuterPress).toHaveBeenCalledTimes(1);
+});
+
+type TestComponentProps = {
+ onPress: () => void;
+ disabled?: boolean;
+};
+const TestComponent = ({ onPress }: TestComponentProps) => {
+ return (
+
+ Trigger Test
+
+ );
+};
+
+test('is not fooled by non-native disabled prop', () => {
+ const handlePress = jest.fn();
+ render();
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger Test'));
+ expect(handlePress).toHaveBeenCalledTimes(1);
+});
+
+type TestChildTouchableComponentProps = {
+ onPress: () => void;
+ someProp: boolean;
+};
+
+function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableComponentProps) {
+ return (
+
+
+ Trigger
+
+
+ );
+}
+
+test('is not fooled by non-responder wrapping host elements', () => {
+ const handlePress = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ deprecated_fireEventSync.press(screen.getByText('Trigger'));
+ expect(handlePress).not.toHaveBeenCalled();
+});
+
+type TestDraggableComponentProps = { onDrag: () => void };
+
+function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) {
+ const responderHandlers = PanResponder.create({
+ onMoveShouldSetPanResponder: (_evt, _gestureState) => true,
+ onPanResponderMove: onDrag,
+ }).panHandlers;
+
+ return (
+
+ Trigger
+
+ );
+}
+
+test('has only onMove', () => {
+ const handleDrag = jest.fn();
+
+ render();
+
+ deprecated_fireEventSync(screen.getByText('Trigger'), 'responderMove', {
+ touchHistory: { mostRecentTimeStamp: '2', touchBank: [] },
+ });
+ expect(handleDrag).toHaveBeenCalled();
+});
+
+// Those events ideally should be triggered through `deprecated_fireEventSync.scroll`, but they are handled at the
+// native level, so we need to support manually triggering them
+describe('native events', () => {
+ test('triggers onScrollBeginDrag', () => {
+ const onScrollBeginDragSpy = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('test-id'), 'onScrollBeginDrag');
+ expect(onScrollBeginDragSpy).toHaveBeenCalled();
+ });
+
+ test('triggers onScrollEndDrag', () => {
+ const onScrollEndDragSpy = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('test-id'), 'onScrollEndDrag');
+ expect(onScrollEndDragSpy).toHaveBeenCalled();
+ });
+
+ test('triggers onMomentumScrollBegin', () => {
+ const onMomentumScrollBeginSpy = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollBegin');
+ expect(onMomentumScrollBeginSpy).toHaveBeenCalled();
+ });
+
+ test('triggers onMomentumScrollEnd', () => {
+ const onMomentumScrollEndSpy = jest.fn();
+ render();
+
+ deprecated_fireEventSync(screen.getByTestId('test-id'), 'onMomentumScrollEnd');
+ expect(onMomentumScrollEndSpy).toHaveBeenCalled();
+ });
+});
+
+test('should handle unmounted elements gracefully', () => {
+ const onPress = jest.fn();
+ render(
+
+ Test
+ ,
+ );
+
+ const element = screen.getByText('Test');
+ screen.unmount();
+
+ // Firing event on unmounted element should not crash
+ deprecated_fireEventSync.press(element);
+ expect(onPress).not.toHaveBeenCalled();
+});
diff --git a/src/__tests__/render-hook-sync.test.tsx b/src/__tests__/deprecated/render-hook-sync.test.tsx
similarity index 98%
rename from src/__tests__/render-hook-sync.test.tsx
rename to src/__tests__/deprecated/render-hook-sync.test.tsx
index 6b269b74a..53a5e2ca8 100644
--- a/src/__tests__/render-hook-sync.test.tsx
+++ b/src/__tests__/deprecated/render-hook-sync.test.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import * as React from 'react';
-import { deprecated_renderHookSync } from '../pure';
+import { deprecated_renderHookSync } from '../../pure';
test('renders hook and returns committed result', () => {
const { result } = deprecated_renderHookSync(() => {
diff --git a/src/__tests__/fire-event-async.test.tsx b/src/__tests__/fire-event-async.test.tsx
deleted file mode 100644
index 49e96be94..000000000
--- a/src/__tests__/fire-event-async.test.tsx
+++ /dev/null
@@ -1,659 +0,0 @@
-import * as React from 'react';
-import {
- PanResponder,
- Pressable,
- ScrollView,
- Text,
- TextInput,
- TouchableOpacity,
- View,
-} from 'react-native';
-
-import { fireEventAsync, render, screen, waitFor } from '..';
-
-type OnPressComponentProps = {
- onPress: () => void;
- text: string;
-};
-const OnPressComponent = ({ onPress, text }: OnPressComponentProps) => (
-
-
- {text}
-
-
-);
-
-type CustomEventComponentProps = {
- onCustomEvent: () => void;
-};
-const CustomEventComponent = ({ onCustomEvent }: CustomEventComponentProps) => (
-
- Custom event component
-
-);
-
-type MyCustomButtonProps = {
- handlePress: () => void;
- text: string;
-};
-const MyCustomButton = ({ handlePress, text }: MyCustomButtonProps) => (
-
-);
-
-type CustomEventComponentWithCustomNameProps = {
- handlePress: () => void;
-};
-const CustomEventComponentWithCustomName = ({
- handlePress,
-}: CustomEventComponentWithCustomNameProps) => (
-
-);
-
-describe('fireEventAsync', () => {
- test('should invoke specified event', async () => {
- const onPressMock = jest.fn();
- render();
-
- await fireEventAsync(screen.getByText('Press me'), 'press');
-
- expect(onPressMock).toHaveBeenCalled();
- });
-
- test('should invoke specified event on parent element', async () => {
- const onPressMock = jest.fn();
- const text = 'New press text';
- render();
-
- await fireEventAsync(screen.getByText(text), 'press');
- expect(onPressMock).toHaveBeenCalled();
- });
-
- test('should invoke event with custom name', async () => {
- const handlerMock = jest.fn();
- const EVENT_DATA = 'event data';
-
- render(
-
-
- ,
- );
-
- await fireEventAsync(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA);
-
- expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA);
- });
-});
-
-test('fireEventAsync.press', async () => {
- const onPressMock = jest.fn();
- const text = 'Fireevent press';
- const eventData = {
- nativeEvent: {
- pageX: 20,
- pageY: 30,
- },
- };
- render();
-
- await fireEventAsync.press(screen.getByText(text), eventData);
-
- expect(onPressMock).toHaveBeenCalledWith(eventData);
-});
-
-test('fireEventAsync.scroll', async () => {
- const onScrollMock = jest.fn();
- const eventData = {
- nativeEvent: {
- contentOffset: {
- y: 200,
- },
- },
- };
-
- render(
-
- XD
- ,
- );
-
- await fireEventAsync.scroll(screen.getByText('XD'), eventData);
-
- expect(onScrollMock).toHaveBeenCalledWith(eventData);
-});
-
-test('fireEventAsync.changeText', async () => {
- const onChangeTextMock = jest.fn();
-
- render(
-
-
- ,
- );
-
- const input = screen.getByPlaceholderText('Customer placeholder');
- await fireEventAsync.changeText(input, 'content');
- expect(onChangeTextMock).toHaveBeenCalledWith('content');
-});
-
-it('sets native state value for unmanaged text inputs', async () => {
- render();
-
- const input = screen.getByTestId('input');
- expect(input).toHaveDisplayValue('');
-
- await fireEventAsync.changeText(input, 'abc');
- expect(input).toHaveDisplayValue('abc');
-});
-
-test('custom component with custom event name', async () => {
- const handlePress = jest.fn();
-
- render();
-
- await fireEventAsync(screen.getByText('Custom component'), 'handlePress');
-
- expect(handlePress).toHaveBeenCalled();
-});
-
-test('event with multiple handler parameters', async () => {
- const handlePress = jest.fn();
-
- render();
-
- await fireEventAsync(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2');
-
- expect(handlePress).toHaveBeenCalledWith('param1', 'param2');
-});
-
-test('should not fire on disabled TouchableOpacity', async () => {
- const handlePress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- expect(handlePress).not.toHaveBeenCalled();
-});
-
-test('should not fire on disabled Pressable', async () => {
- const handlePress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- expect(handlePress).not.toHaveBeenCalled();
-});
-
-test('should not fire inside View with pointerEvents="none"', async () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- await fireEventAsync(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should not fire inside View with pointerEvents="box-only"', async () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- await fireEventAsync(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should fire inside View with pointerEvents="box-none"', async () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- await fireEventAsync(screen.getByText('Trigger'), 'onPress');
- expect(onPress).toHaveBeenCalledTimes(2);
-});
-
-test('should fire inside View with pointerEvents="auto"', async () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- await fireEventAsync(screen.getByText('Trigger'), 'onPress');
- expect(onPress).toHaveBeenCalledTimes(2);
-});
-
-test('should not fire deeply inside View with pointerEvents="box-only"', async () => {
- const onPress = jest.fn();
- render(
-
-
-
- Trigger
-
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- await fireEventAsync(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should fire non-pointer events inside View with pointerEvents="box-none"', async () => {
- const onTouchStart = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('view'), 'touchStart');
- expect(onTouchStart).toHaveBeenCalled();
-});
-
-test('should fire non-touch events inside View with pointerEvents="box-none"', async () => {
- const onLayout = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('view'), 'layout');
- expect(onLayout).toHaveBeenCalled();
-});
-
-// This test if pointerEvents="box-only" on composite `Pressable` is blocking
-// the 'press' event on host View rendered by pressable.
-test('should fire on Pressable with pointerEvents="box-only', async () => {
- const onPress = jest.fn();
- render();
-
- await fireEventAsync.press(screen.getByTestId('pressable'));
- expect(onPress).toHaveBeenCalled();
-});
-
-test('should pass event up on disabled TouchableOpacity', async () => {
- const handleInnerPress = jest.fn();
- const handleOuterPress = jest.fn();
- render(
-
-
- Inner Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Inner Trigger'));
- expect(handleInnerPress).not.toHaveBeenCalled();
- expect(handleOuterPress).toHaveBeenCalledTimes(1);
-});
-
-test('should pass event up on disabled Pressable', async () => {
- const handleInnerPress = jest.fn();
- const handleOuterPress = jest.fn();
- render(
-
-
- Inner Trigger
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Inner Trigger'));
- expect(handleInnerPress).not.toHaveBeenCalled();
- expect(handleOuterPress).toHaveBeenCalledTimes(1);
-});
-
-type TestComponentProps = {
- onPress: () => void;
- disabled?: boolean;
-};
-const TestComponent = ({ onPress }: TestComponentProps) => {
- return (
-
- Trigger Test
-
- );
-};
-
-test('is not fooled by non-native disabled prop', async () => {
- const handlePress = jest.fn();
- render();
-
- await fireEventAsync.press(screen.getByText('Trigger Test'));
- expect(handlePress).toHaveBeenCalledTimes(1);
-});
-
-type TestChildTouchableComponentProps = {
- onPress: () => void;
- someProp: boolean;
-};
-
-function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableComponentProps) {
- return (
-
-
- Trigger
-
-
- );
-}
-
-test('is not fooled by non-responder wrapping host elements', async () => {
- const handlePress = jest.fn();
-
- render(
-
-
- ,
- );
-
- await fireEventAsync.press(screen.getByText('Trigger'));
- expect(handlePress).not.toHaveBeenCalled();
-});
-
-type TestDraggableComponentProps = { onDrag: () => void };
-
-function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) {
- const responderHandlers = PanResponder.create({
- onMoveShouldSetPanResponder: (_evt, _gestureState) => true,
- onPanResponderMove: onDrag,
- }).panHandlers;
-
- return (
-
- Trigger
-
- );
-}
-
-test('has only onMove', async () => {
- const handleDrag = jest.fn();
-
- render();
-
- await fireEventAsync(screen.getByText('Trigger'), 'responderMove', {
- touchHistory: { mostRecentTimeStamp: '2', touchBank: [] },
- });
- expect(handleDrag).toHaveBeenCalled();
-});
-
-// Those events ideally should be triggered through `fireEventAsync.scroll`, but they are handled at the
-// native level, so we need to support manually triggering them
-describe('native events', () => {
- test('triggers onScrollBeginDrag', async () => {
- const onScrollBeginDragSpy = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('test-id'), 'onScrollBeginDrag');
- expect(onScrollBeginDragSpy).toHaveBeenCalled();
- });
-
- test('triggers onScrollEndDrag', async () => {
- const onScrollEndDragSpy = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('test-id'), 'onScrollEndDrag');
- expect(onScrollEndDragSpy).toHaveBeenCalled();
- });
-
- test('triggers onMomentumScrollBegin', async () => {
- const onMomentumScrollBeginSpy = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('test-id'), 'onMomentumScrollBegin');
- expect(onMomentumScrollBeginSpy).toHaveBeenCalled();
- });
-
- test('triggers onMomentumScrollEnd', async () => {
- const onMomentumScrollEndSpy = jest.fn();
- render();
-
- await fireEventAsync(screen.getByTestId('test-id'), 'onMomentumScrollEnd');
- expect(onMomentumScrollEndSpy).toHaveBeenCalled();
- });
-});
-
-describe('React.Suspense integration', () => {
- let mockPromise: Promise;
- let resolveMockPromise: (value: string) => void;
-
- beforeEach(() => {
- mockPromise = new Promise((resolve) => {
- resolveMockPromise = resolve;
- });
- });
-
- type AsyncComponentProps = {
- onPress: () => void;
- shouldSuspend: boolean;
- };
-
- function AsyncComponent({ onPress, shouldSuspend }: AsyncComponentProps) {
- if (shouldSuspend) {
- throw mockPromise;
- }
-
- return (
-
- Async Component Loaded
-
- );
- }
-
- function SuspenseWrapper({ children }: { children: React.ReactNode }) {
- return Loading...}>{children};
- }
-
- test('should handle events after Suspense resolves', async () => {
- const onPressMock = jest.fn();
-
- render(
-
-
- ,
- );
-
- // Initially shows fallback
- expect(screen.getByText('Loading...')).toBeTruthy();
-
- // Resolve the promise
- resolveMockPromise('loaded');
- await waitFor(() => {
- screen.rerender(
-
-
- ,
- );
- });
-
- // Component should be loaded now
- await waitFor(() => {
- expect(screen.getByText('Async Component Loaded')).toBeTruthy();
- });
-
- // fireEventAsync should work on the resolved component
- await fireEventAsync.press(screen.getByText('Async Component Loaded'));
- expect(onPressMock).toHaveBeenCalled();
- });
-
- test('should handle events on Suspense fallback components', async () => {
- const fallbackPressMock = jest.fn();
-
- function InteractiveFallback() {
- return (
-
- Loading with button...
-
- );
- }
-
- render(
- }>
-
- ,
- );
-
- // Should be able to interact with fallback
- expect(screen.getByText('Loading with button...')).toBeTruthy();
-
- await fireEventAsync.press(screen.getByText('Loading with button...'));
- expect(fallbackPressMock).toHaveBeenCalled();
- });
-
- test('should work with nested Suspense boundaries', async () => {
- const outerPressMock = jest.fn();
- const innerPressMock = jest.fn();
-
- type NestedAsyncProps = {
- onPress: () => void;
- shouldSuspend: boolean;
- level: string;
- };
-
- function NestedAsync({ onPress, shouldSuspend, level }: NestedAsyncProps) {
- if (shouldSuspend) {
- throw mockPromise;
- }
-
- return (
-
- {level} Component Loaded
-
- );
- }
-
- const { rerender } = render(
- Outer Loading...}>
-
- Inner Loading...}>
-
-
- ,
- );
-
- // Outer component should be loaded, inner should show fallback
- expect(screen.getByText('Outer Component Loaded')).toBeTruthy();
- expect(screen.getByText('Inner Loading...')).toBeTruthy();
-
- // Should be able to interact with outer component
- await fireEventAsync.press(screen.getByText('Outer Component Loaded'));
- expect(outerPressMock).toHaveBeenCalled();
-
- // Resolve inner component
- resolveMockPromise('inner-loaded');
- await waitFor(() => {
- rerender(
- Outer Loading...}>
-
- Inner Loading...}>
-
-
- ,
- );
- });
-
- // Both components should be loaded now
- await waitFor(() => {
- expect(screen.getByText('Inner Component Loaded')).toBeTruthy();
- });
-
- // Should be able to interact with inner component
- await fireEventAsync.press(screen.getByText('Inner Component Loaded'));
- expect(innerPressMock).toHaveBeenCalled();
- });
-
- test('should work when events cause components to suspend', async () => {
- const onPressMock = jest.fn();
- let shouldSuspend = false;
-
- function DataComponent() {
- if (shouldSuspend) {
- throw mockPromise; // This will cause suspense
- }
- return Data loaded;
- }
-
- function ButtonComponent() {
- return (
- {
- onPressMock();
- shouldSuspend = true; // This will cause DataComponent to suspend on next render
- }}
- >
- Load Data
-
- );
- }
-
- render(
-
-
- Loading data...}>
-
-
- ,
- );
-
- // Initially data is loaded
- expect(screen.getByText('Data loaded')).toBeTruthy();
-
- // Click button - this triggers the state change that will cause suspension
- await fireEventAsync.press(screen.getByText('Load Data'));
- expect(onPressMock).toHaveBeenCalled();
-
- // Rerender - now DataComponent should suspend
- screen.rerender(
-
-
- Loading data...}>
-
-
- ,
- );
-
- // Should show loading fallback
- expect(screen.getByText('Loading data...')).toBeTruthy();
- });
-});
-
-test('should handle unmounted elements gracefully in async mode', async () => {
- const onPress = jest.fn();
- render(
-
- Test
- ,
- );
-
- const element = screen.getByText('Test');
- screen.unmount();
-
- // Firing async event on unmounted element should not crash
- await fireEventAsync.press(element);
- expect(onPress).not.toHaveBeenCalled();
-});
diff --git a/src/__tests__/fire-event-textInput.test.tsx b/src/__tests__/fire-event-textInput.test.tsx
index 7851809e1..9e9f2ccb9 100644
--- a/src/__tests__/fire-event-textInput.test.tsx
+++ b/src/__tests__/fire-event-textInput.test.tsx
@@ -14,7 +14,7 @@ function DoubleWrappedTextInput(props: TextInputProps) {
const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } };
-test('should fire only non-touch-related events on non-editable TextInput', () => {
+test('should fire only non-touch-related events on non-editable TextInput', async () => {
const onFocus = jest.fn();
const onChangeText = jest.fn();
const onSubmitEditing = jest.fn();
@@ -32,10 +32,10 @@ test('should fire only non-touch-related events on non-editable TextInput', () =
);
const subject = screen.getByTestId('subject');
- fireEvent(subject, 'focus');
- fireEvent.changeText(subject, 'Text');
- fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
- fireEvent(subject, 'layout', layoutEvent);
+ await fireEvent(subject, 'focus');
+ await fireEvent.changeText(subject, 'Text');
+ await fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ await fireEvent(subject, 'layout', layoutEvent);
expect(onFocus).not.toHaveBeenCalled();
expect(onChangeText).not.toHaveBeenCalled();
@@ -43,7 +43,7 @@ test('should fire only non-touch-related events on non-editable TextInput', () =
expect(onLayout).toHaveBeenCalledWith(layoutEvent);
});
-test('should fire only non-touch-related events on non-editable TextInput with nested Text', () => {
+test('should fire only non-touch-related events on non-editable TextInput with nested Text', async () => {
const onFocus = jest.fn();
const onChangeText = jest.fn();
const onSubmitEditing = jest.fn();
@@ -63,13 +63,13 @@ test('should fire only non-touch-related events on non-editable TextInput with n
);
const subject = screen.getByText('Nested Text');
- fireEvent(subject, 'focus');
- fireEvent(subject, 'onFocus');
- fireEvent.changeText(subject, 'Text');
- fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
- fireEvent(subject, 'onSubmitEditing', { nativeEvent: { text: 'Text' } });
- fireEvent(subject, 'layout', layoutEvent);
- fireEvent(subject, 'onLayout', layoutEvent);
+ await fireEvent(subject, 'focus');
+ await fireEvent(subject, 'onFocus');
+ await fireEvent.changeText(subject, 'Text');
+ await fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ await fireEvent(subject, 'onSubmitEditing', { nativeEvent: { text: 'Text' } });
+ await fireEvent(subject, 'layout', layoutEvent);
+ await fireEvent(subject, 'onLayout', layoutEvent);
expect(onFocus).not.toHaveBeenCalled();
expect(onChangeText).not.toHaveBeenCalled();
@@ -94,7 +94,7 @@ test('should fire only non-touch-related events on non-editable TextInput with n
* user composite TextInput level, hence invoking the event handlers that
* should be blocked by `editable={false}` prop.
*/
-test('should fire only non-touch-related events on non-editable wrapped TextInput', () => {
+test('should fire only non-touch-related events on non-editable wrapped TextInput', async () => {
const onFocus = jest.fn();
const onChangeText = jest.fn();
const onSubmitEditing = jest.fn();
@@ -112,10 +112,10 @@ test('should fire only non-touch-related events on non-editable wrapped TextInpu
);
const subject = screen.getByTestId('subject');
- fireEvent(subject, 'focus');
- fireEvent.changeText(subject, 'Text');
- fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
- fireEvent(subject, 'layout', layoutEvent);
+ await fireEvent(subject, 'focus');
+ await fireEvent.changeText(subject, 'Text');
+ await fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ await fireEvent(subject, 'layout', layoutEvent);
expect(onFocus).not.toHaveBeenCalled();
expect(onChangeText).not.toHaveBeenCalled();
@@ -126,7 +126,7 @@ test('should fire only non-touch-related events on non-editable wrapped TextInpu
/**
* Ditto testing for even deeper hierarchy of TextInput wrappers.
*/
-test('should fire only non-touch-related events on non-editable double wrapped TextInput', () => {
+test('should fire only non-touch-related events on non-editable double wrapped TextInput', async () => {
const onFocus = jest.fn();
const onChangeText = jest.fn();
const onSubmitEditing = jest.fn();
@@ -144,10 +144,10 @@ test('should fire only non-touch-related events on non-editable double wrapped T
);
const subject = screen.getByTestId('subject');
- fireEvent(subject, 'focus');
- fireEvent.changeText(subject, 'Text');
- fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
- fireEvent(subject, 'layout', layoutEvent);
+ await fireEvent(subject, 'focus');
+ await fireEvent.changeText(subject, 'Text');
+ await fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ await fireEvent(subject, 'layout', layoutEvent);
expect(onFocus).not.toHaveBeenCalled();
expect(onChangeText).not.toHaveBeenCalled();
diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx
index 7e3474bb0..ebf47122c 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/__tests__/fire-event.test.tsx
@@ -9,7 +9,7 @@ import {
View,
} from 'react-native';
-import { fireEvent, render, screen } from '..';
+import { fireEvent, render, screen, waitFor } from '..';
type OnPressComponentProps = {
onPress: () => void;
@@ -50,25 +50,25 @@ const CustomEventComponentWithCustomName = ({
);
describe('fireEvent', () => {
- test('should invoke specified event', () => {
+ test('should invoke specified event', async () => {
const onPressMock = jest.fn();
render();
- fireEvent(screen.getByText('Press me'), 'press');
+ await fireEvent(screen.getByText('Press me'), 'press');
expect(onPressMock).toHaveBeenCalled();
});
- test('should invoke specified event on parent element', () => {
+ test('should invoke specified event on parent element', async () => {
const onPressMock = jest.fn();
const text = 'New press text';
render();
- fireEvent(screen.getByText(text), 'press');
+ await fireEvent(screen.getByText(text), 'press');
expect(onPressMock).toHaveBeenCalled();
});
- test('should invoke event with custom name', () => {
+ test('should invoke event with custom name', async () => {
const handlerMock = jest.fn();
const EVENT_DATA = 'event data';
@@ -78,13 +78,13 @@ describe('fireEvent', () => {
,
);
- fireEvent(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA);
+ await fireEvent(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA);
expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA);
});
});
-test('fireEvent.press', () => {
+test('fireEvent.press', async () => {
const onPressMock = jest.fn();
const text = 'Fireevent press';
const eventData = {
@@ -95,12 +95,12 @@ test('fireEvent.press', () => {
};
render();
- fireEvent.press(screen.getByText(text), eventData);
+ await fireEvent.press(screen.getByText(text), eventData);
expect(onPressMock).toHaveBeenCalledWith(eventData);
});
-test('fireEvent.scroll', () => {
+test('fireEvent.scroll', async () => {
const onScrollMock = jest.fn();
const eventData = {
nativeEvent: {
@@ -116,12 +116,12 @@ test('fireEvent.scroll', () => {
,
);
- fireEvent.scroll(screen.getByText('XD'), eventData);
+ await fireEvent.scroll(screen.getByText('XD'), eventData);
expect(onScrollMock).toHaveBeenCalledWith(eventData);
});
-test('fireEvent.changeText', () => {
+test('fireEvent.changeText', async () => {
const onChangeTextMock = jest.fn();
render(
@@ -131,41 +131,41 @@ test('fireEvent.changeText', () => {
);
const input = screen.getByPlaceholderText('Customer placeholder');
- fireEvent.changeText(input, 'content');
+ await fireEvent.changeText(input, 'content');
expect(onChangeTextMock).toHaveBeenCalledWith('content');
});
-it('sets native state value for unmanaged text inputs', () => {
+it('sets native state value for unmanaged text inputs', async () => {
render();
const input = screen.getByTestId('input');
expect(input).toHaveDisplayValue('');
- fireEvent.changeText(input, 'abc');
+ await fireEvent.changeText(input, 'abc');
expect(input).toHaveDisplayValue('abc');
});
-test('custom component with custom event name', () => {
+test('custom component with custom event name', async () => {
const handlePress = jest.fn();
render();
- fireEvent(screen.getByText('Custom component'), 'handlePress');
+ await fireEvent(screen.getByText('Custom component'), 'handlePress');
expect(handlePress).toHaveBeenCalled();
});
-test('event with multiple handler parameters', () => {
+test('event with multiple handler parameters', async () => {
const handlePress = jest.fn();
render();
- fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2');
+ await fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2');
expect(handlePress).toHaveBeenCalledWith('param1', 'param2');
});
-test('should not fire on disabled TouchableOpacity', () => {
+test('should not fire on disabled TouchableOpacity', async () => {
const handlePress = jest.fn();
render(
@@ -175,11 +175,11 @@ test('should not fire on disabled TouchableOpacity', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent.press(screen.getByText('Trigger'));
expect(handlePress).not.toHaveBeenCalled();
});
-test('should not fire on disabled Pressable', () => {
+test('should not fire on disabled Pressable', async () => {
const handlePress = jest.fn();
render(
@@ -189,11 +189,11 @@ test('should not fire on disabled Pressable', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent.press(screen.getByText('Trigger'));
expect(handlePress).not.toHaveBeenCalled();
});
-test('should not fire inside View with pointerEvents="none" in props', () => {
+test('should not fire inside View with pointerEvents="none"', async () => {
const onPress = jest.fn();
render(
@@ -203,42 +203,12 @@ test('should not fire inside View with pointerEvents="none" in props', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
+ await fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});
-test('should not fire inside View with pointerEvents="none" in styles', () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should not fire inside View with pointerEvents="none" in styles array', () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should not fire inside View with pointerEvents="box-only" in props', () => {
+test('should not fire inside View with pointerEvents="box-only"', async () => {
const onPress = jest.fn();
render(
@@ -248,27 +218,12 @@ test('should not fire inside View with pointerEvents="box-only" in props', () =>
,
);
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should not fire inside View with pointerEvents="box-only" in styles', () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
+ await fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});
-test('should fire inside View with pointerEvents="box-none" in props', () => {
+test('should fire inside View with pointerEvents="box-none"', async () => {
const onPress = jest.fn();
render(
@@ -278,27 +233,12 @@ test('should fire inside View with pointerEvents="box-none" in props', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
+ await fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).toHaveBeenCalledTimes(2);
});
-test('should fire inside View with pointerEvents="box-none" in styles', () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).toHaveBeenCalledTimes(2);
-});
-
-test('should fire inside View with pointerEvents="auto" in props', () => {
+test('should fire inside View with pointerEvents="auto"', async () => {
const onPress = jest.fn();
render(
@@ -308,27 +248,12 @@ test('should fire inside View with pointerEvents="auto" in props', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).toHaveBeenCalledTimes(2);
-});
-
-test('should fire inside View with pointerEvents="auto" in styles', () => {
- const onPress = jest.fn();
- render(
-
-
- Trigger
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
+ await fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).toHaveBeenCalledTimes(2);
});
-test('should not fire deeply inside View with pointerEvents="box-only" in props', () => {
+test('should not fire deeply inside View with pointerEvents="box-only"', async () => {
const onPress = jest.fn();
render(
@@ -340,79 +265,38 @@ test('should not fire deeply inside View with pointerEvents="box-only" in props'
,
);
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
-});
-
-test('should not fire deeply inside View with pointerEvents="box-only" in styles', () => {
- const onPress = jest.fn();
- render(
-
-
-
- Trigger
-
-
- ,
- );
-
- fireEvent.press(screen.getByText('Trigger'));
- fireEvent(screen.getByText('Trigger'), 'onPress');
+ await fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});
-test('should fire non-pointer events inside View with pointerEvents="box-none" in props', () => {
+test('should fire non-pointer events inside View with pointerEvents="box-none"', async () => {
const onTouchStart = jest.fn();
render();
- fireEvent(screen.getByTestId('view'), 'touchStart');
+ await fireEvent(screen.getByTestId('view'), 'touchStart');
expect(onTouchStart).toHaveBeenCalled();
});
-test('should fire non-pointer events inside View with pointerEvents="box-none" in styles', () => {
- const onTouchStart = jest.fn();
- render();
-
- fireEvent(screen.getByTestId('view'), 'touchStart');
- expect(onTouchStart).toHaveBeenCalled();
-});
-
-test('should fire non-touch events inside View with pointerEvents="box-none" in props', () => {
+test('should fire non-touch events inside View with pointerEvents="box-none"', async () => {
const onLayout = jest.fn();
render();
- fireEvent(screen.getByTestId('view'), 'layout');
- expect(onLayout).toHaveBeenCalled();
-});
-
-test('should fire non-touch events inside View with pointerEvents="box-none" in styles', () => {
- const onLayout = jest.fn();
- render();
-
- fireEvent(screen.getByTestId('view'), 'layout');
+ await fireEvent(screen.getByTestId('view'), 'layout');
expect(onLayout).toHaveBeenCalled();
});
// This test if pointerEvents="box-only" on composite `Pressable` is blocking
// the 'press' event on host View rendered by pressable.
-test('should fire on Pressable with pointerEvents="box-only" in props', () => {
+test('should fire on Pressable with pointerEvents="box-only', async () => {
const onPress = jest.fn();
render();
- fireEvent.press(screen.getByTestId('pressable'));
+ await fireEvent.press(screen.getByTestId('pressable'));
expect(onPress).toHaveBeenCalled();
});
-test('should fire on Pressable with pointerEvents="box-only" in styles', () => {
- const onPress = jest.fn();
- render();
-
- fireEvent.press(screen.getByTestId('pressable'));
- expect(onPress).toHaveBeenCalled();
-});
-
-test('should pass event up on disabled TouchableOpacity', () => {
+test('should pass event up on disabled TouchableOpacity', async () => {
const handleInnerPress = jest.fn();
const handleOuterPress = jest.fn();
render(
@@ -423,12 +307,12 @@ test('should pass event up on disabled TouchableOpacity', () => {
,
);
- fireEvent.press(screen.getByText('Inner Trigger'));
+ await fireEvent.press(screen.getByText('Inner Trigger'));
expect(handleInnerPress).not.toHaveBeenCalled();
expect(handleOuterPress).toHaveBeenCalledTimes(1);
});
-test('should pass event up on disabled Pressable', () => {
+test('should pass event up on disabled Pressable', async () => {
const handleInnerPress = jest.fn();
const handleOuterPress = jest.fn();
render(
@@ -439,7 +323,7 @@ test('should pass event up on disabled Pressable', () => {
,
);
- fireEvent.press(screen.getByText('Inner Trigger'));
+ await fireEvent.press(screen.getByText('Inner Trigger'));
expect(handleInnerPress).not.toHaveBeenCalled();
expect(handleOuterPress).toHaveBeenCalledTimes(1);
});
@@ -456,11 +340,11 @@ const TestComponent = ({ onPress }: TestComponentProps) => {
);
};
-test('is not fooled by non-native disabled prop', () => {
+test('is not fooled by non-native disabled prop', async () => {
const handlePress = jest.fn();
render();
- fireEvent.press(screen.getByText('Trigger Test'));
+ await fireEvent.press(screen.getByText('Trigger Test'));
expect(handlePress).toHaveBeenCalledTimes(1);
});
@@ -479,7 +363,7 @@ function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableCo
);
}
-test('is not fooled by non-responder wrapping host elements', () => {
+test('is not fooled by non-responder wrapping host elements', async () => {
const handlePress = jest.fn();
render(
@@ -488,7 +372,7 @@ test('is not fooled by non-responder wrapping host elements', () => {
,
);
- fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent.press(screen.getByText('Trigger'));
expect(handlePress).not.toHaveBeenCalled();
});
@@ -507,12 +391,12 @@ function TestDraggableComponent({ onDrag }: TestDraggableComponentProps) {
);
}
-test('has only onMove', () => {
+test('has only onMove', async () => {
const handleDrag = jest.fn();
render();
- fireEvent(screen.getByText('Trigger'), 'responderMove', {
+ await fireEvent(screen.getByText('Trigger'), 'responderMove', {
touchHistory: { mostRecentTimeStamp: '2', touchBank: [] },
});
expect(handleDrag).toHaveBeenCalled();
@@ -521,40 +405,244 @@ test('has only onMove', () => {
// Those events ideally should be triggered through `fireEvent.scroll`, but they are handled at the
// native level, so we need to support manually triggering them
describe('native events', () => {
- test('triggers onScrollBeginDrag', () => {
+ test('triggers onScrollBeginDrag', async () => {
const onScrollBeginDragSpy = jest.fn();
render();
- fireEvent(screen.getByTestId('test-id'), 'onScrollBeginDrag');
+ await fireEvent(screen.getByTestId('test-id'), 'onScrollBeginDrag');
expect(onScrollBeginDragSpy).toHaveBeenCalled();
});
- test('triggers onScrollEndDrag', () => {
+ test('triggers onScrollEndDrag', async () => {
const onScrollEndDragSpy = jest.fn();
render();
- fireEvent(screen.getByTestId('test-id'), 'onScrollEndDrag');
+ await fireEvent(screen.getByTestId('test-id'), 'onScrollEndDrag');
expect(onScrollEndDragSpy).toHaveBeenCalled();
});
- test('triggers onMomentumScrollBegin', () => {
+ test('triggers onMomentumScrollBegin', async () => {
const onMomentumScrollBeginSpy = jest.fn();
render();
- fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollBegin');
+ await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollBegin');
expect(onMomentumScrollBeginSpy).toHaveBeenCalled();
});
- test('triggers onMomentumScrollEnd', () => {
+ test('triggers onMomentumScrollEnd', async () => {
const onMomentumScrollEndSpy = jest.fn();
render();
- fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollEnd');
+ await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollEnd');
expect(onMomentumScrollEndSpy).toHaveBeenCalled();
});
});
-test('should handle unmounted elements gracefully', () => {
+describe('React.Suspense integration', () => {
+ let mockPromise: Promise;
+ let resolveMockPromise: (value: string) => void;
+
+ beforeEach(() => {
+ mockPromise = new Promise((resolve) => {
+ resolveMockPromise = resolve;
+ });
+ });
+
+ type AsyncComponentProps = {
+ onPress: () => void;
+ shouldSuspend: boolean;
+ };
+
+ function AsyncComponent({ onPress, shouldSuspend }: AsyncComponentProps) {
+ if (shouldSuspend) {
+ throw mockPromise;
+ }
+
+ return (
+
+ Async Component Loaded
+
+ );
+ }
+
+ function SuspenseWrapper({ children }: { children: React.ReactNode }) {
+ return Loading...}>{children};
+ }
+
+ test('should handle events after Suspense resolves', async () => {
+ const onPressMock = jest.fn();
+
+ render(
+
+
+ ,
+ );
+
+ // Initially shows fallback
+ expect(screen.getByText('Loading...')).toBeTruthy();
+
+ // Resolve the promise
+ resolveMockPromise('loaded');
+ await waitFor(() => {
+ screen.rerender(
+
+
+ ,
+ );
+ });
+
+ // Component should be loaded now
+ await waitFor(() => {
+ expect(screen.getByText('Async Component Loaded')).toBeTruthy();
+ });
+
+ // fireEvent should work on the resolved component
+ await fireEvent.press(screen.getByText('Async Component Loaded'));
+ expect(onPressMock).toHaveBeenCalled();
+ });
+
+ test('should handle events on Suspense fallback components', async () => {
+ const fallbackPressMock = jest.fn();
+
+ function InteractiveFallback() {
+ return (
+
+ Loading with button...
+
+ );
+ }
+
+ render(
+ }>
+
+ ,
+ );
+
+ // Should be able to interact with fallback
+ expect(screen.getByText('Loading with button...')).toBeTruthy();
+
+ await fireEvent.press(screen.getByText('Loading with button...'));
+ expect(fallbackPressMock).toHaveBeenCalled();
+ });
+
+ test('should work with nested Suspense boundaries', async () => {
+ const outerPressMock = jest.fn();
+ const innerPressMock = jest.fn();
+
+ type NestedAsyncProps = {
+ onPress: () => void;
+ shouldSuspend: boolean;
+ level: string;
+ };
+
+ function NestedAsync({ onPress, shouldSuspend, level }: NestedAsyncProps) {
+ if (shouldSuspend) {
+ throw mockPromise;
+ }
+
+ return (
+
+ {level} Component Loaded
+
+ );
+ }
+
+ const { rerender } = render(
+ Outer Loading...}>
+
+ Inner Loading...}>
+
+
+ ,
+ );
+
+ // Outer component should be loaded, inner should show fallback
+ expect(screen.getByText('Outer Component Loaded')).toBeTruthy();
+ expect(screen.getByText('Inner Loading...')).toBeTruthy();
+
+ // Should be able to interact with outer component
+ await fireEvent.press(screen.getByText('Outer Component Loaded'));
+ expect(outerPressMock).toHaveBeenCalled();
+
+ // Resolve inner component
+ resolveMockPromise('inner-loaded');
+ await waitFor(() => {
+ rerender(
+ Outer Loading...}>
+
+ Inner Loading...}>
+
+
+ ,
+ );
+ });
+
+ // Both components should be loaded now
+ await waitFor(() => {
+ expect(screen.getByText('Inner Component Loaded')).toBeTruthy();
+ });
+
+ // Should be able to interact with inner component
+ await fireEvent.press(screen.getByText('Inner Component Loaded'));
+ expect(innerPressMock).toHaveBeenCalled();
+ });
+
+ test('should work when events cause components to suspend', async () => {
+ const onPressMock = jest.fn();
+ let shouldSuspend = false;
+
+ function DataComponent() {
+ if (shouldSuspend) {
+ throw mockPromise; // This will cause suspense
+ }
+ return Data loaded;
+ }
+
+ function ButtonComponent() {
+ return (
+ {
+ onPressMock();
+ shouldSuspend = true; // This will cause DataComponent to suspend on next render
+ }}
+ >
+ Load Data
+
+ );
+ }
+
+ render(
+
+
+ Loading data...}>
+
+
+ ,
+ );
+
+ // Initially data is loaded
+ expect(screen.getByText('Data loaded')).toBeTruthy();
+
+ // Click button - this triggers the state change that will cause suspension
+ await fireEvent.press(screen.getByText('Load Data'));
+ expect(onPressMock).toHaveBeenCalled();
+
+ // Rerender - now DataComponent should suspend
+ screen.rerender(
+
+
+ Loading data...}>
+
+
+ ,
+ );
+
+ // Should show loading fallback
+ expect(screen.getByText('Loading data...')).toBeTruthy();
+ });
+});
+
+test('should handle unmounted elements gracefully in async mode', async () => {
const onPress = jest.fn();
render(
@@ -565,7 +653,7 @@ test('should handle unmounted elements gracefully', () => {
const element = screen.getByText('Test');
screen.unmount();
- // Firing event on unmounted element should not crash
- fireEvent.press(element);
+ // Firing async event on unmounted element should not crash
+ await fireEvent.press(element);
expect(onPress).not.toHaveBeenCalled();
});
diff --git a/src/__tests__/react-native-gesture-handler.test.tsx b/src/__tests__/react-native-gesture-handler.test.tsx
index 989ad03cf..eead8ce31 100644
--- a/src/__tests__/react-native-gesture-handler.test.tsx
+++ b/src/__tests__/react-native-gesture-handler.test.tsx
@@ -6,7 +6,7 @@ import { Pressable } from 'react-native-gesture-handler';
import { fireEvent, render, screen, userEvent } from '..';
import { createEventLogger, getEventsNames } from '../test-utils/events';
-test('fireEvent can invoke press events for RNGH Pressable', () => {
+test('fireEvent can invoke press events for RNGH Pressable', async () => {
const onPress = jest.fn();
const onPressIn = jest.fn();
const onPressOut = jest.fn();
@@ -26,16 +26,16 @@ test('fireEvent can invoke press events for RNGH Pressable', () => {
const pressable = screen.getByTestId('pressable');
- fireEvent.press(pressable);
+ await fireEvent.press(pressable);
expect(onPress).toHaveBeenCalled();
- fireEvent(pressable, 'pressIn');
+ await fireEvent(pressable, 'pressIn');
expect(onPressIn).toHaveBeenCalled();
- fireEvent(pressable, 'pressOut');
+ await fireEvent(pressable, 'pressOut');
expect(onPressOut).toHaveBeenCalled();
- fireEvent(pressable, 'longPress');
+ await fireEvent(pressable, 'longPress');
expect(onLongPress).toHaveBeenCalled();
});
diff --git a/src/__tests__/render-debug.test.tsx b/src/__tests__/render-debug.test.tsx
index 16418c19e..544c2e45a 100644
--- a/src/__tests__/render-debug.test.tsx
+++ b/src/__tests__/render-debug.test.tsx
@@ -103,9 +103,9 @@ test('debug', () => {
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('All Props');
});
-test('debug changing component', () => {
+test('debug changing component', async () => {
render();
- fireEvent.press(screen.getByRole('button', { name: 'Change freshness!' }));
+ await fireEvent.press(screen.getByRole('button', { name: 'Change freshness!' }));
screen.debug({ mapProps: null });
diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx
index ba4c21fe5..ae5c8d350 100644
--- a/src/__tests__/render.test.tsx
+++ b/src/__tests__/render.test.tsx
@@ -79,12 +79,12 @@ test('supports basic rendering', () => {
expect(screen.root).toBeOnTheScreen();
});
-test('rerender', () => {
+test('rerender', async () => {
const fn = jest.fn();
render();
expect(fn).toHaveBeenCalledTimes(0);
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
expect(fn).toHaveBeenCalledTimes(1);
screen.rerender();
diff --git a/src/__tests__/wait-for-element-to-be-removed.test.tsx b/src/__tests__/wait-for-element-to-be-removed.test.tsx
index 787b2bce0..0dda4ae87 100644
--- a/src/__tests__/wait-for-element-to-be-removed.test.tsx
+++ b/src/__tests__/wait-for-element-to-be-removed.test.tsx
@@ -32,7 +32,7 @@ afterEach(() => {
test('waits when using getBy query', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
const element = screen.getByText('Observed Element');
expect(element).toBeTruthy();
@@ -44,7 +44,7 @@ test('waits when using getBy query', async () => {
test('waits when using getAllBy query', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
const elements = screen.getAllByText('Observed Element');
expect(elements).toBeTruthy();
@@ -56,7 +56,7 @@ test('waits when using getAllBy query', async () => {
test('waits when using queryBy query', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
const element = screen.getByText('Observed Element');
expect(element).toBeTruthy();
@@ -68,7 +68,7 @@ test('waits when using queryBy query', async () => {
test('waits when using queryAllBy query', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
const elements = screen.getAllByText('Observed Element');
expect(elements).toBeTruthy();
@@ -80,7 +80,7 @@ test('waits when using queryAllBy query', async () => {
test('checks if elements exist at start', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
expect(screen.queryByText('Observed Element')).toBeNull();
await expect(
@@ -93,7 +93,7 @@ test('checks if elements exist at start', async () => {
test('waits until timeout', async () => {
render();
- fireEvent.press(screen.getByText('Remove Element'));
+ await fireEvent.press(screen.getByText('Remove Element'));
expect(screen.getByText('Observed Element')).toBeTruthy();
await expect(
diff --git a/src/__tests__/wait-for.test.tsx b/src/__tests__/wait-for.test.tsx
index 7568d2760..4efd0253c 100644
--- a/src/__tests__/wait-for.test.tsx
+++ b/src/__tests__/wait-for.test.tsx
@@ -40,7 +40,7 @@ afterEach(() => {
test('waits for element until it stops throwing', async () => {
render();
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
expect(screen.queryByText('Fresh')).toBeNull();
@@ -52,7 +52,7 @@ test('waits for element until it stops throwing', async () => {
test('waits for element until timeout is met', async () => {
render();
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow();
@@ -65,7 +65,7 @@ test('waitFor defaults to asyncWaitTimeout config option', async () => {
configure({ asyncUtilTimeout: 100 });
render();
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
await expect(waitFor(() => screen.getByText('Fresh'))).rejects.toThrow();
// Async action ends after 300ms and we only waited 100ms, so we need to wait
@@ -77,7 +77,7 @@ test('waitFor timeout option takes precendence over `asyncWaitTimeout` config op
configure({ asyncUtilTimeout: 2000 });
render();
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
await expect(waitFor(() => screen.getByText('Fresh'), { timeout: 100 })).rejects.toThrow();
// Async action ends after 300ms and we only waited 100ms, so we need to wait
@@ -127,7 +127,7 @@ test('waits for async event with fireEvent', async () => {
const spy = jest.fn();
render();
- fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent.press(screen.getByText('Trigger'));
await waitFor(() => {
expect(spy).toHaveBeenCalled();
@@ -140,7 +140,7 @@ test.each([false, true])(
jest.useFakeTimers({ legacyFakeTimers });
render();
- fireEvent.press(screen.getByText('Change freshness!'));
+ await fireEvent.press(screen.getByText('Change freshness!'));
expect(screen.queryByText('Fresh')).toBeNull();
jest.advanceTimersByTime(300);
@@ -305,7 +305,7 @@ test.each([
await waitFor(() => screen.getByText('red'));
// Check that the `onPress` callback is called with the already-updated value of `syncedColor`.
- fireEvent.press(screen.getByText('Trigger'));
+ await fireEvent.press(screen.getByText('Trigger'));
expect(onPress).toHaveBeenCalledWith('red');
},
);
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 71c32127b..17e1fb119 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -126,7 +126,7 @@ type EventName = StringWithAutocomplete<
| EventNameExtractor
>;
-function fireEvent(element: HostElement, eventName: EventName, ...data: unknown[]) {
+async function fireEvent(element: HostElement, eventName: EventName, ...data: unknown[]) {
if (!isElementMounted(element)) {
return;
}
@@ -139,23 +139,25 @@ function fireEvent(element: HostElement, eventName: EventName, ...data: unknown[
}
let returnValue;
- void act(() => {
+ // eslint-disable-next-line require-await
+ await act(async () => {
returnValue = handler(...data);
});
return returnValue;
}
-fireEvent.press = (element: HostElement, ...data: unknown[]) =>
- fireEvent(element, 'press', ...data);
+fireEvent.press = async (element: HostElement, ...data: unknown[]) =>
+ await fireEvent(element, 'press', ...data);
-fireEvent.changeText = (element: HostElement, ...data: unknown[]) =>
- fireEvent(element, 'changeText', ...data);
+fireEvent.changeText = async (element: HostElement, ...data: unknown[]) =>
+ await fireEvent(element, 'changeText', ...data);
-fireEvent.scroll = (element: HostElement, ...data: unknown[]) =>
- fireEvent(element, 'scroll', ...data);
+fireEvent.scroll = async (element: HostElement, ...data: unknown[]) =>
+ await fireEvent(element, 'scroll', ...data);
-async function fireEventAsync(element: HostElement, eventName: EventName, ...data: unknown[]) {
+/** @deprecated - Use async `fireEvent` instead. */
+function deprecated_fireEventSync(element: HostElement, eventName: EventName, ...data: unknown[]) {
if (!isElementMounted(element)) {
return;
}
@@ -168,25 +170,26 @@ async function fireEventAsync(element: HostElement, eventName: EventName, ...dat
}
let returnValue;
- // eslint-disable-next-line require-await
- await act(async () => {
+ void act(() => {
returnValue = handler(...data);
});
return returnValue;
}
-fireEventAsync.press = async (element: HostElement, ...data: unknown[]) =>
- await fireEventAsync(element, 'press', ...data);
+/** @deprecated - Use async `fireEvent.press` instead. */
+deprecated_fireEventSync.press = (element: HostElement, ...data: unknown[]) =>
+ deprecated_fireEventSync(element, 'press', ...data);
-fireEventAsync.changeText = async (element: HostElement, ...data: unknown[]) =>
- await fireEventAsync(element, 'changeText', ...data);
+/** @deprecated - Use async `fireEvent.changeText` instead. */
+deprecated_fireEventSync.changeText = (element: HostElement, ...data: unknown[]) =>
+ deprecated_fireEventSync(element, 'changeText', ...data);
-fireEventAsync.scroll = async (element: HostElement, ...data: unknown[]) =>
- await fireEventAsync(element, 'scroll', ...data);
+/** @deprecated - Use async `fireEvent.scroll` instead. */
+deprecated_fireEventSync.scroll = (element: HostElement, ...data: unknown[]) =>
+ deprecated_fireEventSync(element, 'scroll', ...data);
-export { fireEventAsync };
-export default fireEvent;
+export { fireEvent, deprecated_fireEventSync };
const scrollEventNames = new Set([
'scroll',
diff --git a/src/pure.ts b/src/pure.ts
index 5503cf968..f36b153e6 100644
--- a/src/pure.ts
+++ b/src/pure.ts
@@ -1,6 +1,6 @@
export { default as act } from './act';
export { default as cleanup, cleanupAsync } from './cleanup';
-export { default as fireEvent, fireEventAsync } from './fire-event';
+export { fireEvent, deprecated_fireEventSync } from './fire-event';
export { default as render } from './render';
export { default as renderAsync } from './render-async';
export { default as waitFor } from './wait-for';
diff --git a/src/queries/__tests__/display-value.test.tsx b/src/queries/__tests__/display-value.test.tsx
index 3da690acd..97d51f0cf 100644
--- a/src/queries/__tests__/display-value.test.tsx
+++ b/src/queries/__tests__/display-value.test.tsx
@@ -203,12 +203,12 @@ test('error message renders the element tree, preserving only helpful props', as
`);
});
-test('supports unmanaged TextInput element', () => {
+test('supports unmanaged TextInput element', async () => {
render();
const input = screen.getByDisplayValue('');
expect(input).toHaveDisplayValue('');
- fireEvent.changeText(input, 'Hello!');
+ await fireEvent.changeText(input, 'Hello!');
expect(input).toHaveDisplayValue('Hello!');
});
diff --git a/src/user-event/scroll/__tests__/scroll-to.test.tsx b/src/user-event/scroll/__tests__/scroll-to.test.tsx
index 29988636b..360c1ff11 100644
--- a/src/user-event/scroll/__tests__/scroll-to.test.tsx
+++ b/src/user-event/scroll/__tests__/scroll-to.test.tsx
@@ -130,7 +130,7 @@ describe('scrollTo()', () => {
const { events } = renderScrollViewWithToolkit();
const user = userEvent.setup();
- fireEvent.scroll(screen.getByTestId('scrollView'), {
+ await fireEvent.scroll(screen.getByTestId('scrollView'), {
nativeEvent: { contentOffset: { y: 100 } },
});
await user.scrollTo(screen.getByTestId('scrollView'), { y: 200 });
diff --git a/typings/index.flow.js b/typings/index.flow.js
index ea383d071..e89c469f2 100644
--- a/typings/index.flow.js
+++ b/typings/index.flow.js
@@ -286,9 +286,21 @@ type FireEventFunction = (
element: ReactTestInstance,
eventName: string,
...data: Array
-) => any;
+) => Promise;
type FireEventAPI = FireEventFunction & {
+ press: (element: ReactTestInstance, ...data: Array) => Promise,
+ changeText: (element: ReactTestInstance, ...data: Array) => Promise,
+ scroll: (element: ReactTestInstance, ...data: Array) => Promise,
+};
+
+type DeprecatedFireEventSyncFunction = (
+ element: ReactTestInstance,
+ eventName: string,
+ ...data: Array
+) => any;
+
+type DeprecatedFireEventSyncAPI = DeprecatedFireEventSyncFunction & {
press: (element: ReactTestInstance, ...data: Array) => any,
changeText: (element: ReactTestInstance, ...data: Array) => any,
scroll: (element: ReactTestInstance, ...data: Array) => any,
@@ -325,6 +337,7 @@ declare module '@testing-library/react-native' {
declare export var cleanup: () => void;
declare export var fireEvent: FireEventAPI;
+ declare export var deprecated_fireEventSync: DeprecatedFireEventSyncAPI;
declare export var waitFor: WaitForFunction;
diff --git a/website/docs/14.x/docs/advanced/understanding-act.mdx b/website/docs/14.x/docs/advanced/understanding-act.mdx
index a6dfd519d..c1b6e2b58 100644
--- a/website/docs/14.x/docs/advanced/understanding-act.mdx
+++ b/website/docs/14.x/docs/advanced/understanding-act.mdx
@@ -87,7 +87,7 @@ Therefore, we should use `act` whenever there is some action that causes element
- re-rendering of component -`renderer.update` call
- triggering any event handlers that cause component tree render
-Thankfully, for these basic cases RNTL has got you covered as our `render`, `update` and `fireEvent` methods already wrap their calls in sync `act` so that you do not have to do it explicitly.
+Thankfully, for these basic cases RNTL has got you covered as our `render`, `update` and `fireEvent` methods already wrap their calls in `act` so that you do not have to do it explicitly.
Note that `act` calls can be safely nested and internally form a stack of calls. However, overlapping `act` calls, which can be achieved using async version of `act`, [are not supported](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js#L161).
diff --git a/website/docs/14.x/docs/api/events/fire-event.mdx b/website/docs/14.x/docs/api/events/fire-event.mdx
index b91b73533..fef427999 100644
--- a/website/docs/14.x/docs/api/events/fire-event.mdx
+++ b/website/docs/14.x/docs/api/events/fire-event.mdx
@@ -10,17 +10,23 @@ Use Fire Event for cases not supported by User Event and for triggering event ha
:::
```ts
-function fireEvent(element: ReactTestInstance, eventName: string, ...data: unknown[]): void;
+function fireEvent(
+ element: ReactTestInstance,
+ eventName: string,
+ ...data: unknown[]
+): Promise;
```
The `fireEvent` API allows you to trigger all kinds of event handlers on both host and composite components. It will try to invoke a single event handler traversing the component tree bottom-up from passed element and trying to find enabled event handler named `onXxx` when `xxx` is the name of the event passed.
Unlike User Event, this API does not automatically pass event object to event handler, this is responsibility of the user to construct such object.
+This function uses async `act` function internally to ensure all pending React updates are executed during event handling.
+
```jsx
import { render, screen, fireEvent } from '@testing-library/react-native';
-test('fire changeText event', () => {
+test('fire changeText event', async () => {
const onEventMock = jest.fn();
render(
// MyComponent renders TextInput which has a placeholder 'Enter details'
@@ -28,7 +34,7 @@ test('fire changeText event', () => {
);
- fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab');
+ await fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab');
expect(onEventMock).toHaveBeenCalledWith('ab');
});
```
@@ -52,7 +58,7 @@ render(
);
// you can omit the `on` prefix
-fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
+await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
```
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`.
@@ -67,7 +73,7 @@ It is recommended to use the User Event [`press()`](docs/api/events/user-event#p
fireEvent.press: (
element: ReactTestInstance,
...data: Array,
-) => void
+) => Promise
```
Invokes `press` event handler on the element or parent element in the tree.
@@ -92,7 +98,7 @@ render(
);
-fireEvent.press(screen.getByText('Press me'), eventData);
+await fireEvent.press(screen.getByText('Press me'), eventData);
expect(onPressMock).toHaveBeenCalledWith(eventData);
```
@@ -106,7 +112,7 @@ It is recommended to use the User Event [`type()`](docs/api/events/user-event#ty
fireEvent.changeText: (
element: ReactTestInstance,
...data: Array,
-) => void
+) => Promise
```
Invokes `changeText` event handler on the element or parent element in the tree.
@@ -124,7 +130,7 @@ render(
);
-fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT);
+await fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT);
```
### `fireEvent.scroll` {#scroll}
@@ -137,7 +143,7 @@ Prefer using [`user.scrollTo`](docs/api/events/user-event#scrollto) over `fireEv
fireEvent.scroll: (
element: ReactTestInstance,
...data: Array,
-) => void
+) => Promise
```
Invokes `scroll` event handler on the element or parent element in the tree.
@@ -163,85 +169,18 @@ render(
);
-fireEvent.scroll(screen.getByText('scroll-view'), eventData);
+await fireEvent.scroll(screen.getByText('scroll-view'), eventData);
```
-:::note
-Prefer using [`user.scrollTo`](docs/api/events/user-event#scrollto) over `fireEvent.scroll` for `ScrollView`, `FlatList`, and `SectionList` components. User Event provides a more realistic event simulation based on React Native runtime behavior.
-:::
-
-## `fireEventAsync` {#fire-event-async}
-
-:::info RNTL minimal version
-
-This API requires RNTL v13.3.0 or later.
-
-:::
+## `deprecated_fireEventSync` {#deprecated-fire-event-sync}
```ts
-async function fireEventAsync(
+function deprecated_fireEventSync(
element: ReactTestInstance,
eventName: string,
...data: unknown[]
-): Promise;
-```
-
-The `fireEventAsync` function is the async version of [`fireEvent`](#fire-event) designed for working with React 19 and React Suspense. This function uses async `act` function internally to ensure all pending React updates are executed during event handling.
-
-```jsx
-import { renderAsync, screen, fireEventAsync } from '@testing-library/react-native';
-
-test('fire event test', async () => {
- await renderAsync();
-
- await fireEventAsync(screen.getByText('Button'), 'press');
- expect(screen.getByText('Action completed')).toBeOnTheScreen();
-});
-```
-
-Like `fireEvent`, `fireEventAsync` also provides convenience methods for common events: `fireEventAsync.press`, `fireEventAsync.changeText`, and `fireEventAsync.scroll`.
-
-### `fireEventAsync.press` {#async-press}
-
-:::note
-It is recommended to use the User Event [`press()`](docs/api/events/user-event#press) helper instead as it offers more realistic simulation of press interaction, including pressable support.
-:::
-
-```tsx
-fireEventAsync.press: (
- element: ReactTestInstance,
- ...data: Array,
-) => Promise
-```
-
-Async version of [`fireEvent.press`](#press) designed for React 19 and React Suspense. Use when `press` event handlers trigger suspense boundaries.
-
-### `fireEventAsync.changeText` {#async-change-text}
-
-:::note
-It is recommended to use the User Event [`type()`](docs/api/events/user-event#type) helper instead as it offers more realistic simulation of text change interaction, including key-by-key typing, element focus, and other editing events.
-:::
-
-```tsx
-fireEventAsync.changeText: (
- element: ReactTestInstance,
- ...data: Array,
-) => Promise
-```
-
-Async version of [`fireEvent.changeText`](#change-text) designed for React 19 and React Suspense. Use when `changeText` event handlers trigger suspense boundaries.
-
-### `fireEventAsync.scroll` {#async-scroll}
-
-:::note
-Prefer using [`user.scrollTo`](docs/api/events/user-event#scrollto) over `fireEventAsync.scroll` for `ScrollView`, `FlatList`, and `SectionList` components. User Event provides a more realistic event simulation based on React Native runtime behavior.
-:::
-
-```tsx
-fireEventAsync.scroll: (
- element: ReactTestInstance,
- ...data: Array,
-) => Promise
+): void;
```
-Async version of [`fireEvent.scroll`](#scroll) designed for React 19 and React Suspense. Use when `scroll` event handlers trigger suspense boundaries.
+Synchronous version of `fireEvent`. Deprecated and will be removed in future versions.
+Use only if you cannot use `await fireEvent` for some reason.
diff --git a/website/docs/14.x/docs/guides/react-19.mdx b/website/docs/14.x/docs/guides/react-19.mdx
index 17ba65b97..cfb61cbbf 100644
--- a/website/docs/14.x/docs/guides/react-19.mdx
+++ b/website/docs/14.x/docs/guides/react-19.mdx
@@ -16,7 +16,7 @@ RNTL 13.3 introduces async versions of the core testing APIs to handle React 19'
**Event APIs:**
-- **[`fireEventAsync`](docs/api/events/fire-event#fire-event-async)** - async version of `fireEvent`
+- **[`fireEvent`](docs/api/events/fire-event)** - updated to be async by default.
## APIs that remain unchanged
diff --git a/website/docs/14.x/docs/migration/v14.mdx b/website/docs/14.x/docs/migration/v14.mdx
index f4476c1bd..49d974199 100644
--- a/website/docs/14.x/docs/migration/v14.mdx
+++ b/website/docs/14.x/docs/migration/v14.mdx
@@ -4,6 +4,63 @@ This guide describes the migration to React Native Testing Library version 14 fr
## Breaking changes
+### `fireEvent` is now async by default
+
+In v14, `fireEvent` and its helpers (`press`, `changeText`, `scroll`) are now async by default and return a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`.
+
+**Before (v13):**
+
+```ts
+import { fireEvent, screen } from '@testing-library/react-native';
+
+it('should press button', () => {
+ render();
+ fireEvent.press(screen.getByText('Press me'));
+ expect(onPress).toHaveBeenCalled();
+});
+```
+
+**After (v14):**
+
+```ts
+import { fireEvent, screen } from '@testing-library/react-native';
+
+it('should press button', async () => {
+ render();
+ await fireEvent.press(screen.getByText('Press me'));
+ expect(onPress).toHaveBeenCalled();
+});
+```
+
+### Migration path
+
+To ease migration, we provide `deprecated_fireEventSync` which maintains the same synchronous behavior as v13. This allows you to migrate gradually.
+
+#### `deprecated_fireEventSync` {#deprecated-fire-event-sync}
+
+```ts
+function deprecated_fireEventSync(
+ element: ReactTestInstance,
+ eventName: string,
+ ...data: unknown[]
+): void;
+```
+
+**⚠️ Deprecated**: This function is provided for migration purposes only. Use async `fireEvent` instead.
+
+The synchronous version of `fireEvent` that returns immediately without awaiting React updates. This maintains backward compatibility with v13 tests but is not recommended for new code.
+
+```ts
+// Old v13 code (still works but deprecated)
+import { deprecated_fireEventSync } from '@testing-library/react-native';
+
+it('should press button', () => {
+ render();
+ deprecated_fireEventSync.press(screen.getByText('Press me'));
+ expect(onPress).toHaveBeenCalled();
+});
+```
+
### `renderHook` is now async by default
In v14, `renderHook` is now async by default and returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`.