From c24449b1afd20751b822d25e2f30b421ba86d3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 28 Jan 2025 16:10:27 +0100 Subject: [PATCH 1/7] feat: testOnly event handlers --- src/__tests__/event-handler.test.tsx | 58 ++++++++++++++++++++++++++ src/event-handler.ts | 38 +++++++++++++++++ src/fire-event.ts | 20 +-------- src/user-event/utils/dispatch-event.ts | 15 +------ 4 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 src/__tests__/event-handler.test.tsx create mode 100644 src/event-handler.ts diff --git a/src/__tests__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx new file mode 100644 index 00000000..dc153f0b --- /dev/null +++ b/src/__tests__/event-handler.test.tsx @@ -0,0 +1,58 @@ +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 00000000..73535948 --- /dev/null +++ b/src/event-handler.ts @@ -0,0 +1,38 @@ +import type { ReactTestInstance } from 'react-test-renderer'; + +export type EventHandlerOptions = { + 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 22477661..a843fad0 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 ec5e1fd8..3f04fb31 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)}`; -} From 3a7f8528a1106520fe46c9853255261cb41082fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 28 Jan 2025 16:36:55 +0100 Subject: [PATCH 2/7] docs --- website/docs/13.x/docs/guides/_meta.json | 2 +- .../docs/guides/third-party-integration.mdx | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 website/docs/13.x/docs/guides/third-party-integration.mdx diff --git a/website/docs/13.x/docs/guides/_meta.json b/website/docs/13.x/docs/guides/_meta.json index 5ae12d7e..866002cd 100644 --- a/website/docs/13.x/docs/guides/_meta.json +++ b/website/docs/13.x/docs/guides/_meta.json @@ -1 +1 @@ -["how-to-query", "troubleshooting", "faq", "community-resources"] +["how-to-query", "troubleshooting", "faq", "community-resources", "third-party-integration"] diff --git a/website/docs/13.x/docs/guides/third-party-integration.mdx b/website/docs/13.x/docs/guides/third-party-integration.mdx new file mode 100644 index 00000000..7cbe46b1 --- /dev/null +++ b/website/docs/13.x/docs/guides/third-party-integration.mdx @@ -0,0 +1,46 @@ +# Third-Party Library Integration + +React Native Testing Library (RNTL) is designed to simulate core React Native behaviors. +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 for simulating events: + +- Fire Event: A lightweight event simulation system that can trigger event handlers defined on both host and composite components. +- User Event: A more realistic interaction simulation system that can only trigger event handlers defined on host components. + +In many third-party libraries, event handling involves native code. In such cases, RNTL cannot +fully simulate the event flow. To address this, you can use testOnly_onXxx props on host components +to expose custom events to RNTL's event subsystems. Both subsystems will first attempt to locate the standard onEvent handlers. If these are not available, they will fall back to testOnly_onXxx handlers. + +### Example: React Native Gesture Handler + +React Native Gesture Handler (RNGH) provides a composite `Pressable` component with `onPress` props. +These props are part of the library's internal event flow, which involves native modules and +`EventEmitters`. As a result, they are not directly exposed to the host components rendered by +`Pressable`, making them inaccessible 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 allows RNTL to simulate +interactions during testing. + +```tsx title="Simplified RNGH Pressable component" +function Pressable({ onPress, onPressIn, onPressOut, ... }) { + // Component logic... + + const isTestEnv = process.env.NODE_ENV === 'test'; + + return ( + + + + ); +} +``` From f1e0309399845d43d6db749c5e0d849bcc5a5b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 28 Jan 2025 16:40:42 +0100 Subject: [PATCH 3/7] . --- .../docs/guides/third-party-integration.mdx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/website/docs/13.x/docs/guides/third-party-integration.mdx b/website/docs/13.x/docs/guides/third-party-integration.mdx index 7cbe46b1..673e66a9 100644 --- a/website/docs/13.x/docs/guides/third-party-integration.mdx +++ b/website/docs/13.x/docs/guides/third-party-integration.mdx @@ -1,6 +1,6 @@ # Third-Party Library Integration -React Native Testing Library (RNTL) is designed to simulate core React Native behaviors. +React Native Testing Library is designed to simulate core React Native behaviors. However, it does not replicate the internal logic of third-party libraries. This guide explains how to integrate your library with RNTL. @@ -17,30 +17,32 @@ to expose custom events to RNTL's event subsystems. Both subsystems will first a ### Example: React Native Gesture Handler -React Native Gesture Handler (RNGH) provides a composite `Pressable` component with `onPress` props. +React Native Gesture Handler (RNGH) provides a composite [Pressable](https://docs.swmansion.com/react-native-gesture-handler/docs/components/pressable/) +component with `onPressXxx` props. These props are part of the library's internal event flow, which involves native modules and `EventEmitters`. As a result, they are not directly exposed to the host components rendered by `Pressable`, making them inaccessible to RNTL's event subsystems. -To enable RNTL to interact with RNGH's `Pressable` component, the library exposes `testOnly_onPress` +To enable RNTL to interact with RNGH's `Pressable` component, the library exposes `testOnly_onPressXxx` props on the `NativeButton` host component rendered by `Pressable`. This allows RNTL to simulate interactions during testing. ```tsx title="Simplified RNGH Pressable component" function Pressable({ onPress, onPressIn, onPressOut, ... }) { - // Component logic... - - const isTestEnv = process.env.NODE_ENV === 'test'; - - return ( - - + + // Component logic... + + const isTestEnv = process.env.NODE_ENV === 'test'; + + return ( + + - ); + ); } ``` From 7600515f4bba83e506cb52aab54a962c957acc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 28 Jan 2025 16:45:04 +0100 Subject: [PATCH 4/7] . --- src/__tests__/event-handler.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx index dc153f0b..ca518672 100644 --- a/src/__tests__/event-handler.test.tsx +++ b/src/__tests__/event-handler.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Text, View } from 'react-native'; -import { render, screen } from '../..'; +import { render, screen } from '..'; import { getEventHandler } from '../event-handler'; test('getEventHandler strict mode', () => { From 1ad1cef9a7eae419f74e593aa770fbdfdfe0f827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 5 Feb 2025 10:07:48 +0100 Subject: [PATCH 5/7] . --- website/docs/12.x/docs/api.md | 6 +-- website/docs/13.x/docs/advanced/_meta.json | 2 +- .../docs/advanced/third-party-integration.mdx | 39 +++++++++++++++ website/docs/13.x/docs/api.md | 6 +-- website/docs/13.x/docs/guides/_meta.json | 2 +- .../docs/guides/third-party-integration.mdx | 48 ------------------- 6 files changed, 47 insertions(+), 56 deletions(-) create mode 100644 website/docs/13.x/docs/advanced/third-party-integration.mdx delete mode 100644 website/docs/13.x/docs/guides/third-party-integration.mdx diff --git a/website/docs/12.x/docs/api.md b/website/docs/12.x/docs/api.md index ef91c3ae..ced5fd96 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 32b909fa..1449c6c5 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 00000000..4cf9ba02 --- /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 ef91c3ae..ced5fd96 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` diff --git a/website/docs/13.x/docs/guides/_meta.json b/website/docs/13.x/docs/guides/_meta.json index 866002cd..5ae12d7e 100644 --- a/website/docs/13.x/docs/guides/_meta.json +++ b/website/docs/13.x/docs/guides/_meta.json @@ -1 +1 @@ -["how-to-query", "troubleshooting", "faq", "community-resources", "third-party-integration"] +["how-to-query", "troubleshooting", "faq", "community-resources"] diff --git a/website/docs/13.x/docs/guides/third-party-integration.mdx b/website/docs/13.x/docs/guides/third-party-integration.mdx deleted file mode 100644 index 673e66a9..00000000 --- a/website/docs/13.x/docs/guides/third-party-integration.mdx +++ /dev/null @@ -1,48 +0,0 @@ -# Third-Party Library Integration - -React Native Testing Library is designed to simulate core React Native behaviors. -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 for simulating events: - -- Fire Event: A lightweight event simulation system that can trigger event handlers defined on both host and composite components. -- User Event: A more realistic interaction simulation system that can only trigger event handlers defined on host components. - -In many third-party libraries, event handling involves native code. In such cases, RNTL cannot -fully simulate the event flow. To address this, you can use testOnly_onXxx props on host components -to expose custom events to RNTL's event subsystems. Both subsystems will first attempt to locate the standard onEvent handlers. If these are not available, they will fall back to testOnly_onXxx 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 `onPressXxx` props. -These props are part of the library's internal event flow, which involves native modules and -`EventEmitters`. As a result, they are not directly exposed to the host components rendered by -`Pressable`, making them inaccessible to RNTL's event subsystems. - -To enable RNTL to interact with RNGH's `Pressable` component, the library exposes `testOnly_onPressXxx` -props on the `NativeButton` host component rendered by `Pressable`. This allows RNTL to simulate -interactions during testing. - -```tsx title="Simplified RNGH Pressable component" -function Pressable({ onPress, onPressIn, onPressOut, ... }) { - - // Component logic... - - const isTestEnv = process.env.NODE_ENV === 'test'; - - return ( - - - - ); -} -``` From ab5c687fd54b7052cd27e6666d17401e41ceb037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 5 Feb 2025 10:12:18 +0100 Subject: [PATCH 6/7] . --- src/event-handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/event-handler.ts b/src/event-handler.ts index 73535948..8f275c6b 100644 --- a/src/event-handler.ts +++ b/src/event-handler.ts @@ -1,6 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; export type EventHandlerOptions = { + /** Include check for event handler named without adding `on*` prefix. */ loose?: boolean; }; From 9ef72d5b6800e91c22c66955c3b6511df45e00ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 5 Feb 2025 10:14:27 +0100 Subject: [PATCH 7/7] . --- src/__tests__/event-handler.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/event-handler.test.tsx b/src/__tests__/event-handler.test.tsx index ca518672..2b4eb577 100644 --- a/src/__tests__/event-handler.test.tsx +++ b/src/__tests__/event-handler.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Text, View } from 'react-native'; + import { render, screen } from '..'; import { getEventHandler } from '../event-handler';