diff --git a/src/__tests__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx
new file mode 100644
index 000000000..2b4eb577e
--- /dev/null
+++ b/src/__tests__/event-handler.test.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { Text, View } from 'react-native';
+
+import { render, screen } from '..';
+import { getEventHandler } from '../event-handler';
+
+test('getEventHandler strict mode', () => {
+ const onPress = jest.fn();
+ const testOnlyOnPress = jest.fn();
+
+ render(
+
+
+ {/* @ts-expect-error Intentionally passing such props */}
+
+ {/* @ts-expect-error Intentionally passing such props */}
+
+ ,
+ );
+
+ const regular = screen.getByTestId('regular');
+ const testOnly = screen.getByTestId('testOnly');
+ const both = screen.getByTestId('both');
+
+ expect(getEventHandler(regular, 'press')).toBe(onPress);
+ expect(getEventHandler(testOnly, 'press')).toBe(testOnlyOnPress);
+ expect(getEventHandler(both, 'press')).toBe(onPress);
+
+ expect(getEventHandler(regular, 'onPress')).toBe(undefined);
+ expect(getEventHandler(testOnly, 'onPress')).toBe(undefined);
+ expect(getEventHandler(both, 'onPress')).toBe(undefined);
+});
+
+test('getEventHandler loose mode', () => {
+ const onPress = jest.fn();
+ const testOnlyOnPress = jest.fn();
+
+ render(
+
+
+ {/* @ts-expect-error Intentionally passing such props */}
+
+ {/* @ts-expect-error Intentionally passing such props */}
+
+ ,
+ );
+
+ const regular = screen.getByTestId('regular');
+ const testOnly = screen.getByTestId('testOnly');
+ const both = screen.getByTestId('both');
+
+ expect(getEventHandler(regular, 'press', { loose: true })).toBe(onPress);
+ expect(getEventHandler(testOnly, 'press', { loose: true })).toBe(testOnlyOnPress);
+ expect(getEventHandler(both, 'press', { loose: true })).toBe(onPress);
+
+ expect(getEventHandler(regular, 'onPress', { loose: true })).toBe(onPress);
+ expect(getEventHandler(testOnly, 'onPress', { loose: true })).toBe(testOnlyOnPress);
+ expect(getEventHandler(both, 'onPress', { loose: true })).toBe(onPress);
+});
diff --git a/src/event-handler.ts b/src/event-handler.ts
new file mode 100644
index 000000000..8f275c6b4
--- /dev/null
+++ b/src/event-handler.ts
@@ -0,0 +1,39 @@
+import type { ReactTestInstance } from 'react-test-renderer';
+
+export type EventHandlerOptions = {
+ /** Include check for event handler named without adding `on*` prefix. */
+ loose?: boolean;
+};
+
+export function getEventHandler(
+ element: ReactTestInstance,
+ eventName: string,
+ options?: EventHandlerOptions,
+) {
+ const handlerName = getEventHandlerName(eventName);
+ if (typeof element.props[handlerName] === 'function') {
+ return element.props[handlerName];
+ }
+
+ if (options?.loose && typeof element.props[eventName] === 'function') {
+ return element.props[eventName];
+ }
+
+ if (typeof element.props[`testOnly_${handlerName}`] === 'function') {
+ return element.props[`testOnly_${handlerName}`];
+ }
+
+ if (options?.loose && typeof element.props[`testOnly_${eventName}`] === 'function') {
+ return element.props[`testOnly_${eventName}`];
+ }
+
+ return undefined;
+}
+
+export function getEventHandlerName(eventName: string) {
+ return `on${capitalizeFirstLetter(eventName)}`;
+}
+
+function capitalizeFirstLetter(str: string) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 224776618..a843fad09 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -8,6 +8,7 @@ import type {
import type { ReactTestInstance } from 'react-test-renderer';
import act from './act';
+import { getEventHandler } from './event-handler';
import { isElementMounted, isHostElement } from './helpers/component-tree';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
@@ -80,7 +81,7 @@ function findEventHandler(
): EventHandler | null {
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
- const handler = getEventHandler(element, eventName);
+ const handler = getEventHandler(element, eventName, { loose: true });
if (handler && isEventEnabled(element, eventName, touchResponder)) return handler;
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
@@ -91,23 +92,6 @@ function findEventHandler(
return findEventHandler(element.parent, eventName, touchResponder);
}
-function getEventHandler(element: ReactTestInstance, eventName: string) {
- const eventHandlerName = getEventHandlerName(eventName);
- if (typeof element.props[eventHandlerName] === 'function') {
- return element.props[eventHandlerName];
- }
-
- if (typeof element.props[eventName] === 'function') {
- return element.props[eventName];
- }
-
- return undefined;
-}
-
-function getEventHandlerName(eventName: string) {
- return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
-}
-
// String union type of keys of T that start with on, stripped of 'on'
type EventNameExtractor = keyof {
[K in keyof T as K extends `on${infer Rest}` ? Uncapitalize : never]: T[K];
diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts
index ec5e1fd80..3f04fb31d 100644
--- a/src/user-event/utils/dispatch-event.ts
+++ b/src/user-event/utils/dispatch-event.ts
@@ -1,6 +1,7 @@
import type { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
+import { getEventHandler } from '../../event-handler';
import { isElementMounted } from '../../helpers/component-tree';
/**
@@ -25,17 +26,3 @@ export function dispatchEvent(element: ReactTestInstance, eventName: string, ...
handler(...event);
});
}
-
-function getEventHandler(element: ReactTestInstance, eventName: string) {
- const handleName = getEventHandlerName(eventName);
- const handle = element.props[handleName] as unknown;
- if (typeof handle !== 'function') {
- return undefined;
- }
-
- return handle;
-}
-
-function getEventHandlerName(eventName: string) {
- return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
-}
diff --git a/website/docs/12.x/docs/api.md b/website/docs/12.x/docs/api.md
index ef91c3ae8..ced5fd96a 100644
--- a/website/docs/12.x/docs/api.md
+++ b/website/docs/12.x/docs/api.md
@@ -1,6 +1,7 @@
---
uri: /api
---
+
# API Overview
React Native Testing Library consists of following APIs:
@@ -12,10 +13,9 @@ React Native Testing Library consists of following APIs:
- Helpers: [`debug`](docs/api/screen#debug), [`toJSON`](docs/api/screen#tojson), [`root`](docs/api/screen#root)
- [Jest matchers](docs/api/jest-matchers) - validate assumptions about your UI
- [User Event](docs/api/events/user-event) - simulate common user interactions like [`press`](docs/api/events/user-event#press) or [`type`](docs/api/events/user-event#type) in a realistic way
-- [Fire Event](docs/api/events/fire-event) - simulate any component event in a simplified way
-purposes
+- [Fire Event](docs/api/events/fire-event) - simulate any component event in a simplified way purposes
- Misc APIs:
- - [`renderHook` function](docs/api/misc/render-hook) - render hooks for testing
+ - [`renderHook` function](docs/api/misc/render-hook) - render hooks for testing
- [Async utils](docs/api/misc/async): `findBy*` queries, `wait`, `waitForElementToBeRemoved`
- [Configuration](docs/api/misc/config): `configure`, `resetToDefaults`
- [Accessibility](docs/api/misc/accessibility): `isHiddenFromAccessibility`
diff --git a/website/docs/13.x/docs/advanced/_meta.json b/website/docs/13.x/docs/advanced/_meta.json
index 32b909fa0..1449c6c52 100644
--- a/website/docs/13.x/docs/advanced/_meta.json
+++ b/website/docs/13.x/docs/advanced/_meta.json
@@ -1 +1 @@
-["testing-env", "understanding-act"]
+["testing-env", "understanding-act", "third-party-integration"]
diff --git a/website/docs/13.x/docs/advanced/third-party-integration.mdx b/website/docs/13.x/docs/advanced/third-party-integration.mdx
new file mode 100644
index 000000000..4cf9ba025
--- /dev/null
+++ b/website/docs/13.x/docs/advanced/third-party-integration.mdx
@@ -0,0 +1,39 @@
+# Third-Party Library Integration
+
+The React Native Testing Library is designed to simulate the core behaviors of React Native. However, it does not replicate the internal logic of third-party libraries. This guide explains how to integrate your library with RNTL.
+
+## Handling Events in Third-Party Libraries
+
+RNTL provides two subsystems to simulate events:
+
+- **Fire Event**: A lightweight simulation system that can trigger event handlers defined on both host and composite components.
+- **User Event**: A more realistic interaction simulation system that can trigger event handlers defined only on host components.
+
+In many third-party libraries, event handling involves native code, which means RNTL cannot fully simulate the event flow, as it runs only JavaScript code. To address this limitation, you can use `testOnly_on*` props on host components to expose custom events to RNTL’s event subsystems. Both subsystems will first attempt to locate the standard `on*` event handlers; if these are not available, they fall back to the `testOnly_on*` handlers.
+
+### Example: React Native Gesture Handler
+
+React Native Gesture Handler (RNGH) provides a composite [Pressable](https://docs.swmansion.com/react-native-gesture-handler/docs/components/pressable/) component with `onPress*` props. These event handlers are not exposed on the rendered host views; instead, they are invoked via RNGH’s internal event flow, which involves native modules. As a result, they are not accessible to RNTL’s event subsystems.
+
+To enable RNTL to interact with RNGH’s `Pressable` component, the library exposes `testOnly_onPress*` props on the `NativeButton` host component rendered by `Pressable`. This adjustment allows RNTL to simulate interactions during testing.
+
+```tsx title="Simplified RNGH Pressable component"
+function Pressable({ onPress, onPressIn, onPressOut, onLongPress, ... }) {
+
+ // Component logic...
+
+ const isTestEnv = process.env.NODE_ENV === 'test';
+
+ return (
+
+
+
+ );
+}
+```
diff --git a/website/docs/13.x/docs/api.md b/website/docs/13.x/docs/api.md
index ef91c3ae8..ced5fd96a 100644
--- a/website/docs/13.x/docs/api.md
+++ b/website/docs/13.x/docs/api.md
@@ -1,6 +1,7 @@
---
uri: /api
---
+
# API Overview
React Native Testing Library consists of following APIs:
@@ -12,10 +13,9 @@ React Native Testing Library consists of following APIs:
- Helpers: [`debug`](docs/api/screen#debug), [`toJSON`](docs/api/screen#tojson), [`root`](docs/api/screen#root)
- [Jest matchers](docs/api/jest-matchers) - validate assumptions about your UI
- [User Event](docs/api/events/user-event) - simulate common user interactions like [`press`](docs/api/events/user-event#press) or [`type`](docs/api/events/user-event#type) in a realistic way
-- [Fire Event](docs/api/events/fire-event) - simulate any component event in a simplified way
-purposes
+- [Fire Event](docs/api/events/fire-event) - simulate any component event in a simplified way purposes
- Misc APIs:
- - [`renderHook` function](docs/api/misc/render-hook) - render hooks for testing
+ - [`renderHook` function](docs/api/misc/render-hook) - render hooks for testing
- [Async utils](docs/api/misc/async): `findBy*` queries, `wait`, `waitForElementToBeRemoved`
- [Configuration](docs/api/misc/config): `configure`, `resetToDefaults`
- [Accessibility](docs/api/misc/accessibility): `isHiddenFromAccessibility`