From 5b3a7c3ca32d53b59b6c662cf2f1b8d7f247216f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:07:56 +0100 Subject: [PATCH 01/21] . --- src/__tests__/{fire-event.test.tsx => fire-event-old.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{fire-event.test.tsx => fire-event-old.test.tsx} (100%) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event-old.test.tsx similarity index 100% rename from src/__tests__/fire-event.test.tsx rename to src/__tests__/fire-event-old.test.tsx From a6b986cf0ccf6c5c95d55ea440507788951d4899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 22:48:59 +0100 Subject: [PATCH 02/21] basic tests . edge cases . --- src/__tests__/fire-event.test.tsx | 232 ++++++++++++++++++++++++++++++ src/fire-event.ts | 7 +- 2 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/fire-event.test.tsx diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx new file mode 100644 index 000000000..3802f96af --- /dev/null +++ b/src/__tests__/fire-event.test.tsx @@ -0,0 +1,232 @@ +import * as React from 'react'; +import { + Platform, + Pressable, + ScrollView, + Text, + TextInput, + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native'; + +import { fireEvent, render, screen } from '..'; +import { nativeState } from '../native-state'; + +describe('fireEvent.press', () => { + test('works on Pressable', async () => { + const onPress = jest.fn(); + await render(); + await fireEvent.press(screen.getByTestId('pressable')); + expect(onPress).toHaveBeenCalled(); + }); + + test('works on TouchableOpacity', async () => { + const onPress = jest.fn(); + await render( + + Press me + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }); + + test('works on TouchableHighlight', async () => { + const onPress = jest.fn(); + await render( + + Press me + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }); + + test('works on TouchableWithoutFeedback', async () => { + const onPress = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }); + + test('works on TouchableNativeFeedback', async () => { + if (Platform.OS !== 'android') { + return; + } + + const onPress = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }); +}); + +test('fireEvent.changeText works on TextInput', async () => { + const onChangeText = jest.fn(); + await render(); + await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + expect(onChangeText).toHaveBeenCalledWith('new text'); +}); + +test('fireEvent.scroll works on ScrollView', async () => { + const onScroll = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { y: 200 } } }; + await render( + + Content + , + ); + await fireEvent.scroll(screen.getByTestId('scroll'), eventData); + expect(onScroll).toHaveBeenCalledWith(eventData); +}); + +test('fireEvent bubbles event to parent handler', async () => { + const onPress = jest.fn(); + await render( + + Press me + , + ); + await fireEvent.press(screen.getByText('Press me')); + expect(onPress).toHaveBeenCalled(); +}); + +test('fireEvent accepts event name with or without "on" prefix', async () => { + const onPress = jest.fn(); + await render(); + + await fireEvent(screen.getByTestId('btn'), 'press'); + expect(onPress).toHaveBeenCalledTimes(1); + + await fireEvent(screen.getByTestId('btn'), 'onPress'); + expect(onPress).toHaveBeenCalledTimes(2); +}); + +test('fireEvent does not fire on elements with pointerEvents="none"', async () => { + const onPress = jest.fn(); + await render( + + + , + ); + await fireEvent.press(screen.getByTestId('btn')); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('fireEvent.changeText does not fire on non-editable TextInput', async () => { + const onChangeText = jest.fn(); + await render(); + await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + expect(onChangeText).not.toHaveBeenCalled(); +}); + +test('fireEvent.changeText updates native state for uncontrolled TextInput', async () => { + await render(); + const input = screen.getByTestId('input'); + await fireEvent.changeText(input, 'hello'); + expect(input).toHaveDisplayValue('hello'); + expect(nativeState.valueForElement.get(input)).toBe('hello'); +}); + +test('fireEvent returns handler return value', async () => { + const handler = jest.fn().mockReturnValue('result'); + await render(); + const result = await fireEvent.press(screen.getByTestId('btn')); + expect(result).toBe('result'); +}); + +test('fireEvent does nothing when element is unmounted', async () => { + const onPress = jest.fn(); + const { unmount } = await render(); + const element = screen.getByTestId('btn'); + + await unmount(); + + await fireEvent.press(element); + expect(onPress).not.toHaveBeenCalled(); +}); + +describe('edge cases', () => { + test('scroll event without contentOffset does not update native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, {}); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined(); + }); + + test('scroll event with non-finite contentOffset values uses 0', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, { + nativeEvent: { contentOffset: { x: Infinity, y: NaN } }, + }); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); + }); + + test('scroll event with valid x and y contentOffset updates native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, { + nativeEvent: { contentOffset: { x: 100, y: 200 } }, + }); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 100, y: 200 }); + }); + + test('layout event fires on non-editable TextInput', async () => { + const onLayout = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('input'), 'layout'); + expect(onLayout).toHaveBeenCalled(); + }); + + test('scroll event fires on non-editable TextInput', async () => { + const onScroll = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('input'), 'scroll'); + expect(onScroll).toHaveBeenCalled(); + }); + + test('does not fire when onStartShouldSetResponder returns false', async () => { + const onPress = jest.fn(); + await render( + false} onPress={onPress}> + Press + , + ); + await fireEvent.press(screen.getByTestId('text')); + expect(onPress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/fire-event.ts b/src/fire-event.ts index 96276643d..0a1a176f6 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -10,7 +10,7 @@ import type { Fiber, HostElement } from 'test-renderer'; import { act } from './act'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; -import { isElementMounted, isHostElement } from './helpers/component-tree'; +import { isElementMounted } from './helpers/component-tree'; import { isHostScrollView, isHostTextInput } from './helpers/host-component-names'; import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; @@ -18,10 +18,6 @@ import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; export function isTouchResponder(element: HostElement) { - if (!isHostElement(element)) { - return false; - } - return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element); } @@ -184,6 +180,7 @@ function tryGetContentOffset(event: unknown): Point | null { const contentOffset = event?.nativeEvent?.contentOffset; const x = contentOffset?.x; const y = contentOffset?.y; + if (typeof x === 'number' || typeof y === 'number') { return { x: Number.isFinite(x) ? x : 0, From a329c668f81a5693ce17a7adb562a041c324ee76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:21:55 +0100 Subject: [PATCH 03/21] . --- src/__tests__/fire-event-old.test.tsx | 659 -------------------------- src/__tests__/fire-event.test.tsx | 277 ++++++++++- 2 files changed, 269 insertions(+), 667 deletions(-) delete mode 100644 src/__tests__/fire-event-old.test.tsx diff --git a/src/__tests__/fire-event-old.test.tsx b/src/__tests__/fire-event-old.test.tsx deleted file mode 100644 index 965663504..000000000 --- a/src/__tests__/fire-event-old.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 { fireEvent, 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('fireEvent', () => { - test('should invoke specified event', async () => { - const onPressMock = jest.fn(); - await render(); - - await fireEvent(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'; - await render(); - - await fireEvent(screen.getByText(text), 'press'); - expect(onPressMock).toHaveBeenCalled(); - }); - - test('should invoke event with custom name', async () => { - const handlerMock = jest.fn(); - const EVENT_DATA = 'event data'; - - await render( - - - , - ); - - await fireEvent(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA); - - expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA); - }); -}); - -test('fireEvent.press', async () => { - const onPressMock = jest.fn(); - const text = 'Fireevent press'; - const eventData = { - nativeEvent: { - pageX: 20, - pageY: 30, - }, - }; - await render(); - - await fireEvent.press(screen.getByText(text), eventData); - - expect(onPressMock).toHaveBeenCalledWith(eventData); -}); - -test('fireEvent.scroll', async () => { - const onScrollMock = jest.fn(); - const eventData = { - nativeEvent: { - contentOffset: { - y: 200, - }, - }, - }; - - await render( - - XD - , - ); - - await fireEvent.scroll(screen.getByText('XD'), eventData); - - expect(onScrollMock).toHaveBeenCalledWith(eventData); -}); - -test('fireEvent.changeText', async () => { - const onChangeTextMock = jest.fn(); - - await render( - - - , - ); - - const input = screen.getByPlaceholderText('Customer placeholder'); - await fireEvent.changeText(input, 'content'); - expect(onChangeTextMock).toHaveBeenCalledWith('content'); -}); - -it('sets native state value for unmanaged text inputs', async () => { - await render(); - - const input = screen.getByTestId('input'); - expect(input).toHaveDisplayValue(''); - - await fireEvent.changeText(input, 'abc'); - expect(input).toHaveDisplayValue('abc'); -}); - -test('custom component with custom event name', async () => { - const handlePress = jest.fn(); - - await render(); - - await fireEvent(screen.getByText('Custom component'), 'handlePress'); - - expect(handlePress).toHaveBeenCalled(); -}); - -test('event with multiple handler parameters', async () => { - const handlePress = jest.fn(); - - await render(); - - await fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2'); - - expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); -}); - -test('should not fire on disabled TouchableOpacity', async () => { - const handlePress = jest.fn(); - await render( - - - Trigger - - , - ); - - await fireEvent.press(screen.getByText('Trigger')); - expect(handlePress).not.toHaveBeenCalled(); -}); - -test('should not fire on disabled Pressable', async () => { - const handlePress = jest.fn(); - await render( - - - Trigger - - , - ); - - await fireEvent.press(screen.getByText('Trigger')); - expect(handlePress).not.toHaveBeenCalled(); -}); - -test('should not fire inside View with pointerEvents="none"', async () => { - const onPress = jest.fn(); - await render( - - - Trigger - - , - ); - - await fireEvent.press(screen.getByText('Trigger')); - await fireEvent(screen.getByText('Trigger'), 'onPress'); - expect(onPress).not.toHaveBeenCalled(); -}); - -test('should not fire inside View with pointerEvents="box-only"', async () => { - const onPress = jest.fn(); - await render( - - - Trigger - - , - ); - - 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"', async () => { - const onPress = jest.fn(); - await render( - - - Trigger - - , - ); - - await fireEvent.press(screen.getByText('Trigger')); - await fireEvent(screen.getByText('Trigger'), 'onPress'); - expect(onPress).toHaveBeenCalledTimes(2); -}); - -test('should fire inside View with pointerEvents="auto"', async () => { - const onPress = jest.fn(); - await render( - - - Trigger - - , - ); - - 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"', async () => { - const onPress = jest.fn(); - await render( - - - - Trigger - - - , - ); - - 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"', async () => { - const onTouchStart = jest.fn(); - await render(); - - await fireEvent(screen.getByTestId('view'), 'touchStart'); - expect(onTouchStart).toHaveBeenCalled(); -}); - -test('should fire non-touch events inside View with pointerEvents="box-none"', async () => { - const onLayout = jest.fn(); - await render(); - - 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', async () => { - const onPress = jest.fn(); - await render(); - - await fireEvent.press(screen.getByTestId('pressable')); - expect(onPress).toHaveBeenCalled(); -}); - -test('should pass event up on disabled TouchableOpacity', async () => { - const handleInnerPress = jest.fn(); - const handleOuterPress = jest.fn(); - await render( - - - 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', async () => { - const handleInnerPress = jest.fn(); - const handleOuterPress = jest.fn(); - await render( - - - Inner Trigger - - , - ); - - await fireEvent.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(); - await render(); - - await fireEvent.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(); - - await render( - - - , - ); - - await fireEvent.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(); - - await render(); - - await fireEvent(screen.getByText('Trigger'), 'responderMove', { - touchHistory: { mostRecentTimeStamp: '2', touchBank: [] }, - }); - expect(handleDrag).toHaveBeenCalled(); -}); - -// 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', async () => { - const onScrollBeginDragSpy = jest.fn(); - await render(); - - await fireEvent(screen.getByTestId('test-id'), 'onScrollBeginDrag'); - expect(onScrollBeginDragSpy).toHaveBeenCalled(); - }); - - test('triggers onScrollEndDrag', async () => { - const onScrollEndDragSpy = jest.fn(); - await render(); - - await fireEvent(screen.getByTestId('test-id'), 'onScrollEndDrag'); - expect(onScrollEndDragSpy).toHaveBeenCalled(); - }); - - test('triggers onMomentumScrollBegin', async () => { - const onMomentumScrollBeginSpy = jest.fn(); - await render(); - - await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollBegin'); - expect(onMomentumScrollBeginSpy).toHaveBeenCalled(); - }); - - test('triggers onMomentumScrollEnd', async () => { - const onMomentumScrollEndSpy = jest.fn(); - await render(); - - await fireEvent(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(); - - await render( - - - , - ); - - // Initially shows fallback - expect(screen.getByText('Loading...')).toBeTruthy(); - - // Resolve the promise - resolveMockPromise('loaded'); - await waitFor(async () => { - await 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... - - ); - } - - await 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 } = await 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(async () => { - await 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 - - ); - } - - await 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 - await screen.rerender( - - - Loading data...}> - - - , - ); - - // Should show loading fallback - expect(screen.getByText('Loading data...')).toBeTruthy(); - }); -}); - -test('should handle unmounted elements gracefully', async () => { - const onPress = jest.fn(); - await render( - - Test - , - ); - - const element = screen.getByText('Test'); - await screen.rerender(); - - // Firing async event on unmounted element should not crash - await fireEvent.press(element); - expect(onPress).not.toHaveBeenCalled(); -}); diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 3802f96af..f8ede52cc 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { + PanResponder, Platform, Pressable, ScrollView, @@ -117,15 +118,225 @@ test('fireEvent accepts event name with or without "on" prefix', async () => { expect(onPress).toHaveBeenCalledTimes(2); }); -test('fireEvent does not fire on elements with pointerEvents="none"', async () => { +test('fireEvent.press passes event data to handler', async () => { const onPress = jest.fn(); - await render( - - - , - ); - await fireEvent.press(screen.getByTestId('btn')); - expect(onPress).not.toHaveBeenCalled(); + const eventData = { nativeEvent: { pageX: 20, pageY: 30 } }; + await render(); + await fireEvent.press(screen.getByTestId('btn'), eventData); + expect(onPress).toHaveBeenCalledWith(eventData); +}); + +test('fireEvent passes multiple parameters to handler', async () => { + const handlePress = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2'); + expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); +}); + +describe('custom events', () => { + test('fires custom event with data', async () => { + const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( + onCustomEvent('event data')}> + Custom + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); + expect(handler).toHaveBeenCalledWith('event data'); + }); + + test('fires event with custom prop name on composite component', async () => { + const MyButton = ({ handlePress }: { handlePress: () => void }) => ( + + Button + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Button'), 'handlePress'); + expect(handler).toHaveBeenCalled(); + }); +}); + +describe('disabled elements', () => { + test('does not fire on disabled TouchableOpacity', async () => { + const onPress = jest.fn(); + await render( + + Trigger + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).not.toHaveBeenCalled(); + }); + + test('does not fire on disabled Pressable', async () => { + const onPress = jest.fn(); + await render( + + Trigger + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).not.toHaveBeenCalled(); + }); + + test('bubbles event past disabled inner to enabled outer TouchableOpacity', async () => { + const handleInnerPress = jest.fn(); + const handleOuterPress = jest.fn(); + await render( + + + Inner Trigger + + , + ); + await fireEvent.press(screen.getByText('Inner Trigger')); + expect(handleInnerPress).not.toHaveBeenCalled(); + expect(handleOuterPress).toHaveBeenCalledTimes(1); + }); + + test('bubbles event past disabled inner to enabled outer Pressable', async () => { + const handleInnerPress = jest.fn(); + const handleOuterPress = jest.fn(); + await render( + + + Inner Trigger + + , + ); + await fireEvent.press(screen.getByText('Inner Trigger')); + expect(handleInnerPress).not.toHaveBeenCalled(); + expect(handleOuterPress).toHaveBeenCalledTimes(1); + }); + + test('is not fooled by non-native disabled prop on composite component', async () => { + const TestComponent = ({ onPress }: { onPress: () => void; disabled?: boolean }) => ( + + Trigger Test + + ); + const handlePress = jest.fn(); + await render(); + await fireEvent.press(screen.getByText('Trigger Test')); + expect(handlePress).toHaveBeenCalledTimes(1); + }); + + test('respects disabled prop through composite wrappers', async () => { + function TestChildTouchableComponent({ + onPress, + someProp, + }: { + onPress: () => void; + someProp: boolean; + }) { + return ( + + + Trigger + + + ); + } + const handlePress = jest.fn(); + await render( + + + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(handlePress).not.toHaveBeenCalled(); + }); +}); + +describe('pointerEvents prop', () => { + test('does not fire inside View with pointerEvents="none"', async () => { + const onPress = jest.fn(); + await render( + + + , + ); + await fireEvent.press(screen.getByTestId('btn')); + expect(onPress).not.toHaveBeenCalled(); + }); + + test('does not fire inside View with pointerEvents="box-only"', async () => { + const onPress = jest.fn(); + await render( + + + Trigger + + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).not.toHaveBeenCalled(); + }); + + test('fires inside View with pointerEvents="box-none"', async () => { + const onPress = jest.fn(); + await render( + + + Trigger + + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + test('fires inside View with pointerEvents="auto"', async () => { + const onPress = jest.fn(); + await render( + + + Trigger + + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + test('does not fire deeply inside View with pointerEvents="box-only"', async () => { + const onPress = jest.fn(); + await render( + + + + Trigger + + + , + ); + await fireEvent.press(screen.getByText('Trigger')); + expect(onPress).not.toHaveBeenCalled(); + }); + + test('fires non-pointer events inside View with pointerEvents="box-none"', async () => { + const onTouchStart = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('view'), 'touchStart'); + expect(onTouchStart).toHaveBeenCalled(); + }); + + test('fires layout event inside View with pointerEvents="box-none"', async () => { + const onLayout = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('view'), 'layout'); + expect(onLayout).toHaveBeenCalled(); + }); + + test('fires on Pressable with pointerEvents="box-only" on itself', async () => { + const onPress = jest.fn(); + await render(); + await fireEvent.press(screen.getByTestId('pressable')); + expect(onPress).toHaveBeenCalled(); + }); }); test('fireEvent.changeText does not fire on non-editable TextInput', async () => { @@ -229,4 +440,54 @@ describe('edge cases', () => { await fireEvent.press(screen.getByTestId('text')); expect(onPress).not.toHaveBeenCalled(); }); + + test('fires responderMove on PanResponder component', async () => { + const onDrag = jest.fn(); + function TestDraggableComponent({ onDrag }: { onDrag: () => void }) { + const responderHandlers = PanResponder.create({ + onMoveShouldSetPanResponder: () => true, + onPanResponderMove: onDrag, + }).panHandlers; + return ( + + Trigger + + ); + } + await render(); + await fireEvent(screen.getByText('Trigger'), 'responderMove', { + touchHistory: { mostRecentTimeStamp: '2', touchBank: [] }, + }); + expect(onDrag).toHaveBeenCalled(); + }); +}); + +describe('native ScrollView events', () => { + test('fires onScrollBeginDrag', async () => { + const onScrollBeginDrag = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'scrollBeginDrag'); + expect(onScrollBeginDrag).toHaveBeenCalled(); + }); + + test('fires onScrollEndDrag', async () => { + const onScrollEndDrag = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'scrollEndDrag'); + expect(onScrollEndDrag).toHaveBeenCalled(); + }); + + test('fires onMomentumScrollBegin', async () => { + const onMomentumScrollBegin = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'momentumScrollBegin'); + expect(onMomentumScrollBegin).toHaveBeenCalled(); + }); + + test('fires onMomentumScrollEnd', async () => { + const onMomentumScrollEnd = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'momentumScrollEnd'); + expect(onMomentumScrollEnd).toHaveBeenCalled(); + }); }); From dd9421481fbef32214e92f710a939b926f351e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:28:01 +0100 Subject: [PATCH 04/21] . --- src/__tests__/fire-event.test.tsx | 352 +++++++++++++++--------------- 1 file changed, 177 insertions(+), 175 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index f8ede52cc..b1bcce1bb 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -16,6 +16,85 @@ import { import { fireEvent, render, screen } from '..'; import { nativeState } from '../native-state'; +test('fireEvent accepts event name with or without "on" prefix', async () => { + const onPress = jest.fn(); + await render(); + + await fireEvent(screen.getByTestId('btn'), 'press'); + expect(onPress).toHaveBeenCalledTimes(1); + + await fireEvent(screen.getByTestId('btn'), 'onPress'); + expect(onPress).toHaveBeenCalledTimes(2); +}); + +test('fireEvent passes event data to handler', async () => { + const onPress = jest.fn(); + const eventData = { nativeEvent: { pageX: 20, pageY: 30 } }; + await render(); + await fireEvent.press(screen.getByTestId('btn'), eventData); + expect(onPress).toHaveBeenCalledWith(eventData); +}); + +test('fireEvent passes multiple parameters to handler', async () => { + const handlePress = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2'); + expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); +}); + +test('fireEvent bubbles event to parent handler', async () => { + const onPress = jest.fn(); + await render( + + Press me + , + ); + await fireEvent.press(screen.getByText('Press me')); + expect(onPress).toHaveBeenCalled(); +}); + +test('fireEvent returns handler return value', async () => { + const handler = jest.fn().mockReturnValue('result'); + await render(); + const result = await fireEvent.press(screen.getByTestId('btn')); + expect(result).toBe('result'); +}); + +test('fireEvent does nothing when element is unmounted', async () => { + const onPress = jest.fn(); + const { unmount } = await render(); + const element = screen.getByTestId('btn'); + + await unmount(); + + await fireEvent.press(element); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('fireEvent fires custom event on composite component', async () => { + const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( + onCustomEvent('event data')}> + Custom + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); + expect(handler).toHaveBeenCalledWith('event data'); +}); + +test('fireEvent fires event with custom prop name on composite component', async () => { + const MyButton = ({ handlePress }: { handlePress: () => void }) => ( + + Button + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Button'), 'handlePress'); + expect(handler).toHaveBeenCalled(); +}); + describe('fireEvent.press', () => { test('works on Pressable', async () => { const onPress = jest.fn(); @@ -77,85 +156,112 @@ describe('fireEvent.press', () => { }); }); -test('fireEvent.changeText works on TextInput', async () => { - const onChangeText = jest.fn(); - await render(); - await fireEvent.changeText(screen.getByTestId('input'), 'new text'); - expect(onChangeText).toHaveBeenCalledWith('new text'); -}); +describe('fireEvent.changeText', () => { + test('works on TextInput', async () => { + const onChangeText = jest.fn(); + await render(); + await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + expect(onChangeText).toHaveBeenCalledWith('new text'); + }); -test('fireEvent.scroll works on ScrollView', async () => { - const onScroll = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { y: 200 } } }; - await render( - - Content - , - ); - await fireEvent.scroll(screen.getByTestId('scroll'), eventData); - expect(onScroll).toHaveBeenCalledWith(eventData); -}); + test('does not fire on non-editable TextInput', async () => { + const onChangeText = jest.fn(); + await render(); + await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + expect(onChangeText).not.toHaveBeenCalled(); + }); -test('fireEvent bubbles event to parent handler', async () => { - const onPress = jest.fn(); - await render( - - Press me - , - ); - await fireEvent.press(screen.getByText('Press me')); - expect(onPress).toHaveBeenCalled(); + test('updates native state for uncontrolled TextInput', async () => { + await render(); + const input = screen.getByTestId('input'); + await fireEvent.changeText(input, 'hello'); + expect(input).toHaveDisplayValue('hello'); + expect(nativeState.valueForElement.get(input)).toBe('hello'); + }); }); -test('fireEvent accepts event name with or without "on" prefix', async () => { - const onPress = jest.fn(); - await render(); +describe('fireEvent.scroll', () => { + test('works on ScrollView', async () => { + const onScroll = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { y: 200 } } }; + await render( + + Content + , + ); + await fireEvent.scroll(screen.getByTestId('scroll'), eventData); + expect(onScroll).toHaveBeenCalledWith(eventData); + }); - await fireEvent(screen.getByTestId('btn'), 'press'); - expect(onPress).toHaveBeenCalledTimes(1); + test('fires onScrollBeginDrag', async () => { + const onScrollBeginDrag = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'scrollBeginDrag'); + expect(onScrollBeginDrag).toHaveBeenCalled(); + }); - await fireEvent(screen.getByTestId('btn'), 'onPress'); - expect(onPress).toHaveBeenCalledTimes(2); -}); + test('fires onScrollEndDrag', async () => { + const onScrollEndDrag = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'scrollEndDrag'); + expect(onScrollEndDrag).toHaveBeenCalled(); + }); -test('fireEvent.press passes event data to handler', async () => { - const onPress = jest.fn(); - const eventData = { nativeEvent: { pageX: 20, pageY: 30 } }; - await render(); - await fireEvent.press(screen.getByTestId('btn'), eventData); - expect(onPress).toHaveBeenCalledWith(eventData); -}); + test('fires onMomentumScrollBegin', async () => { + const onMomentumScrollBegin = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'momentumScrollBegin'); + expect(onMomentumScrollBegin).toHaveBeenCalled(); + }); -test('fireEvent passes multiple parameters to handler', async () => { - const handlePress = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2'); - expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); -}); + test('fires onMomentumScrollEnd', async () => { + const onMomentumScrollEnd = jest.fn(); + await render(); + await fireEvent(screen.getByTestId('scroll'), 'momentumScrollEnd'); + expect(onMomentumScrollEnd).toHaveBeenCalled(); + }); -describe('custom events', () => { - test('fires custom event with data', async () => { - const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( - onCustomEvent('event data')}> - Custom - + test('without contentOffset does not update native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , ); - const handler = jest.fn(); - await render(); - await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); - expect(handler).toHaveBeenCalledWith('event data'); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, {}); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined(); }); - test('fires event with custom prop name on composite component', async () => { - const MyButton = ({ handlePress }: { handlePress: () => void }) => ( - - Button - + test('with non-finite contentOffset values uses 0', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, { + nativeEvent: { contentOffset: { x: Infinity, y: NaN } }, + }); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); + }); + + test('with valid contentOffset updates native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , ); - const handler = jest.fn(); - await render(); - await fireEvent(screen.getByText('Button'), 'handlePress'); - expect(handler).toHaveBeenCalled(); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, { + nativeEvent: { contentOffset: { x: 100, y: 200 } }, + }); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 100, y: 200 }); }); }); @@ -339,97 +445,23 @@ describe('pointerEvents prop', () => { }); }); -test('fireEvent.changeText does not fire on non-editable TextInput', async () => { - const onChangeText = jest.fn(); - await render(); - await fireEvent.changeText(screen.getByTestId('input'), 'new text'); - expect(onChangeText).not.toHaveBeenCalled(); -}); - -test('fireEvent.changeText updates native state for uncontrolled TextInput', async () => { - await render(); - const input = screen.getByTestId('input'); - await fireEvent.changeText(input, 'hello'); - expect(input).toHaveDisplayValue('hello'); - expect(nativeState.valueForElement.get(input)).toBe('hello'); -}); - -test('fireEvent returns handler return value', async () => { - const handler = jest.fn().mockReturnValue('result'); - await render(); - const result = await fireEvent.press(screen.getByTestId('btn')); - expect(result).toBe('result'); -}); - -test('fireEvent does nothing when element is unmounted', async () => { - const onPress = jest.fn(); - const { unmount } = await render(); - const element = screen.getByTestId('btn'); - - await unmount(); - - await fireEvent.press(element); - expect(onPress).not.toHaveBeenCalled(); -}); - -describe('edge cases', () => { - test('scroll event without contentOffset does not update native state', async () => { - const onScroll = jest.fn(); - await render( - - Content - , - ); - const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, {}); - expect(onScroll).toHaveBeenCalled(); - expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined(); - }); - - test('scroll event with non-finite contentOffset values uses 0', async () => { - const onScroll = jest.fn(); - await render( - - Content - , - ); - const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, { - nativeEvent: { contentOffset: { x: Infinity, y: NaN } }, - }); - expect(onScroll).toHaveBeenCalled(); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); - }); - - test('scroll event with valid x and y contentOffset updates native state', async () => { - const onScroll = jest.fn(); - await render( - - Content - , - ); - const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, { - nativeEvent: { contentOffset: { x: 100, y: 200 } }, - }); - expect(onScroll).toHaveBeenCalled(); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 100, y: 200 }); - }); - - test('layout event fires on non-editable TextInput', async () => { +describe('non-editable TextInput', () => { + test('fires layout event', async () => { const onLayout = jest.fn(); await render(); await fireEvent(screen.getByTestId('input'), 'layout'); expect(onLayout).toHaveBeenCalled(); }); - test('scroll event fires on non-editable TextInput', async () => { + test('fires scroll event', async () => { const onScroll = jest.fn(); await render(); await fireEvent(screen.getByTestId('input'), 'scroll'); expect(onScroll).toHaveBeenCalled(); }); +}); +describe('responder system', () => { test('does not fire when onStartShouldSetResponder returns false', async () => { const onPress = jest.fn(); await render( @@ -461,33 +493,3 @@ describe('edge cases', () => { expect(onDrag).toHaveBeenCalled(); }); }); - -describe('native ScrollView events', () => { - test('fires onScrollBeginDrag', async () => { - const onScrollBeginDrag = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('scroll'), 'scrollBeginDrag'); - expect(onScrollBeginDrag).toHaveBeenCalled(); - }); - - test('fires onScrollEndDrag', async () => { - const onScrollEndDrag = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('scroll'), 'scrollEndDrag'); - expect(onScrollEndDrag).toHaveBeenCalled(); - }); - - test('fires onMomentumScrollBegin', async () => { - const onMomentumScrollBegin = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('scroll'), 'momentumScrollBegin'); - expect(onMomentumScrollBegin).toHaveBeenCalled(); - }); - - test('fires onMomentumScrollEnd', async () => { - const onMomentumScrollEnd = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('scroll'), 'momentumScrollEnd'); - expect(onMomentumScrollEnd).toHaveBeenCalled(); - }); -}); From 92c09b9a6968e25f1b48425f2adedea17d6ac5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:28:24 +0100 Subject: [PATCH 05/21] native state --- src/__tests__/fire-event.test.tsx | 40 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index b1bcce1bb..421ce4057 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -160,15 +160,19 @@ describe('fireEvent.changeText', () => { test('works on TextInput', async () => { const onChangeText = jest.fn(); await render(); - await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + const input = screen.getByTestId('input'); + await fireEvent.changeText(input, 'new text'); expect(onChangeText).toHaveBeenCalledWith('new text'); + expect(nativeState.valueForElement.get(input)).toBe('new text'); }); test('does not fire on non-editable TextInput', async () => { const onChangeText = jest.fn(); await render(); - await fireEvent.changeText(screen.getByTestId('input'), 'new text'); + const input = screen.getByTestId('input'); + await fireEvent.changeText(input, 'new text'); expect(onChangeText).not.toHaveBeenCalled(); + expect(nativeState.valueForElement.get(input)).toBeUndefined(); }); test('updates native state for uncontrolled TextInput', async () => { @@ -189,36 +193,50 @@ describe('fireEvent.scroll', () => { Content , ); - await fireEvent.scroll(screen.getByTestId('scroll'), eventData); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, eventData); expect(onScroll).toHaveBeenCalledWith(eventData); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); }); test('fires onScrollBeginDrag', async () => { const onScrollBeginDrag = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { x: 50, y: 100 } } }; await render(); - await fireEvent(screen.getByTestId('scroll'), 'scrollBeginDrag'); - expect(onScrollBeginDrag).toHaveBeenCalled(); + const scrollView = screen.getByTestId('scroll'); + await fireEvent(scrollView, 'scrollBeginDrag', eventData); + expect(onScrollBeginDrag).toHaveBeenCalledWith(eventData); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 100 }); }); test('fires onScrollEndDrag', async () => { const onScrollEndDrag = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { x: 75, y: 150 } } }; await render(); - await fireEvent(screen.getByTestId('scroll'), 'scrollEndDrag'); - expect(onScrollEndDrag).toHaveBeenCalled(); + const scrollView = screen.getByTestId('scroll'); + await fireEvent(scrollView, 'scrollEndDrag', eventData); + expect(onScrollEndDrag).toHaveBeenCalledWith(eventData); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 75, y: 150 }); }); test('fires onMomentumScrollBegin', async () => { const onMomentumScrollBegin = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { x: 120, y: 250 } } }; await render(); - await fireEvent(screen.getByTestId('scroll'), 'momentumScrollBegin'); - expect(onMomentumScrollBegin).toHaveBeenCalled(); + const scrollView = screen.getByTestId('scroll'); + await fireEvent(scrollView, 'momentumScrollBegin', eventData); + expect(onMomentumScrollBegin).toHaveBeenCalledWith(eventData); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 120, y: 250 }); }); test('fires onMomentumScrollEnd', async () => { const onMomentumScrollEnd = jest.fn(); + const eventData = { nativeEvent: { contentOffset: { x: 200, y: 400 } } }; await render(); - await fireEvent(screen.getByTestId('scroll'), 'momentumScrollEnd'); - expect(onMomentumScrollEnd).toHaveBeenCalled(); + const scrollView = screen.getByTestId('scroll'); + await fireEvent(scrollView, 'momentumScrollEnd', eventData); + expect(onMomentumScrollEnd).toHaveBeenCalledWith(eventData); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 200, y: 400 }); }); test('without contentOffset does not update native state', async () => { From ef08305014c92b8e0d9f5c152767ead83c48a92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:31:11 +0100 Subject: [PATCH 06/21] . --- src/__tests__/fire-event-textInput.test.tsx | 156 -------------------- src/__tests__/fire-event.test.tsx | 107 ++++++++++++++ 2 files changed, 107 insertions(+), 156 deletions(-) delete mode 100644 src/__tests__/fire-event-textInput.test.tsx diff --git a/src/__tests__/fire-event-textInput.test.tsx b/src/__tests__/fire-event-textInput.test.tsx deleted file mode 100644 index 7223b1b21..000000000 --- a/src/__tests__/fire-event-textInput.test.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react'; -import type { TextInputProps } from 'react-native'; -import { Text, TextInput } from 'react-native'; - -import { fireEvent, render, screen } from '..'; - -function WrappedTextInput(props: TextInputProps) { - return ; -} - -function DoubleWrappedTextInput(props: TextInputProps) { - return ; -} - -const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; - -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(); - const onLayout = jest.fn(); - - await render( - , - ); - - const subject = screen.getByTestId('subject'); - 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(); - expect(onSubmitEditing).not.toHaveBeenCalled(); - expect(onLayout).toHaveBeenCalledWith(layoutEvent); -}); - -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(); - const onLayout = jest.fn(); - - await render( - - Nested Text - , - ); - - const subject = screen.getByText('Nested Text'); - 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(); - expect(onSubmitEditing).not.toHaveBeenCalled(); - expect(onLayout).toHaveBeenCalledTimes(2); - expect(onLayout).toHaveBeenCalledWith(layoutEvent); -}); - -/** - * Historically there were problems with custom TextInput wrappers, as they - * could creat a hierarchy of three or more composite text input views with - * very similar event props. - * - * Typical hierarchy would be: - * - User composite TextInput - * - UI library composite TextInput - * - RN composite TextInput - * - RN host TextInput - * - * Previous implementation of fireEvent only checked `editable` prop for - * RN TextInputs, both host & composite but did not check on the UI library or - * 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', async () => { - const onFocus = jest.fn(); - const onChangeText = jest.fn(); - const onSubmitEditing = jest.fn(); - const onLayout = jest.fn(); - - await render( - , - ); - - const subject = screen.getByTestId('subject'); - 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(); - expect(onSubmitEditing).not.toHaveBeenCalled(); - expect(onLayout).toHaveBeenCalledWith(layoutEvent); -}); - -/** - * Ditto testing for even deeper hierarchy of TextInput wrappers. - */ -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(); - const onLayout = jest.fn(); - - await render( - , - ); - - const subject = screen.getByTestId('subject'); - 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(); - expect(onSubmitEditing).not.toHaveBeenCalled(); - expect(onLayout).toHaveBeenCalledWith(layoutEvent); -}); diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 421ce4057..8b6e54967 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { TextInputProps } from 'react-native'; import { PanResponder, Platform, @@ -16,6 +17,14 @@ import { import { fireEvent, render, screen } from '..'; import { nativeState } from '../native-state'; +function WrappedTextInput(props: TextInputProps) { + return ; +} + +function DoubleWrappedTextInput(props: TextInputProps) { + return ; +} + test('fireEvent accepts event name with or without "on" prefix', async () => { const onPress = jest.fn(); await render(); @@ -464,6 +473,104 @@ describe('pointerEvents prop', () => { }); describe('non-editable TextInput', () => { + const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; + + test('blocks touch-related events but allows non-touch events', async () => { + const onFocus = jest.fn(); + const onChangeText = jest.fn(); + const onSubmitEditing = jest.fn(); + const onLayout = jest.fn(); + + await render( + , + ); + + const input = screen.getByTestId('input'); + await fireEvent(input, 'focus'); + await fireEvent.changeText(input, 'Text'); + await fireEvent(input, 'submitEditing', { nativeEvent: { text: 'Text' } }); + await fireEvent(input, 'layout', layoutEvent); + + expect(onFocus).not.toHaveBeenCalled(); + expect(onChangeText).not.toHaveBeenCalled(); + expect(onSubmitEditing).not.toHaveBeenCalled(); + expect(onLayout).toHaveBeenCalledWith(layoutEvent); + }); + + test('blocks touch-related events when firing on nested Text child', async () => { + const onFocus = jest.fn(); + const onChangeText = jest.fn(); + const onSubmitEditing = jest.fn(); + const onLayout = jest.fn(); + + await render( + + Nested Text + , + ); + + const subject = screen.getByText('Nested Text'); + 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(); + expect(onSubmitEditing).not.toHaveBeenCalled(); + expect(onLayout).toHaveBeenCalledTimes(2); + expect(onLayout).toHaveBeenCalledWith(layoutEvent); + }); + + test.each([ + ['WrappedTextInput', WrappedTextInput], + ['DoubleWrappedTextInput', DoubleWrappedTextInput], + ])('blocks touch-related events on %s', async (_, Component) => { + const onFocus = jest.fn(); + const onChangeText = jest.fn(); + const onSubmitEditing = jest.fn(); + const onLayout = jest.fn(); + + await render( + , + ); + + const input = screen.getByTestId('input'); + await fireEvent(input, 'focus'); + await fireEvent.changeText(input, 'Text'); + await fireEvent(input, 'submitEditing', { nativeEvent: { text: 'Text' } }); + await fireEvent(input, 'layout', layoutEvent); + + expect(onFocus).not.toHaveBeenCalled(); + expect(onChangeText).not.toHaveBeenCalled(); + expect(onSubmitEditing).not.toHaveBeenCalled(); + expect(onLayout).toHaveBeenCalledWith(layoutEvent); + }); + test('fires layout event', async () => { const onLayout = jest.fn(); await render(); From 0afa8cde15cbc3e7521add40fed0e67862ae9f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:36:00 +0100 Subject: [PATCH 07/21] ai review --- src/__tests__/fire-event.test.tsx | 198 +++++++++++++++++++++++------- 1 file changed, 156 insertions(+), 42 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 8b6e54967..34538afb3 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -17,13 +17,9 @@ import { import { fireEvent, render, screen } from '..'; import { nativeState } from '../native-state'; -function WrappedTextInput(props: TextInputProps) { - return ; -} - -function DoubleWrappedTextInput(props: TextInputProps) { - return ; -} +// Shared test data +const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; +const pressEventData = { nativeEvent: { pageX: 20, pageY: 30 } }; test('fireEvent accepts event name with or without "on" prefix', async () => { const onPress = jest.fn(); @@ -38,17 +34,16 @@ test('fireEvent accepts event name with or without "on" prefix', async () => { test('fireEvent passes event data to handler', async () => { const onPress = jest.fn(); - const eventData = { nativeEvent: { pageX: 20, pageY: 30 } }; await render(); - await fireEvent.press(screen.getByTestId('btn'), eventData); - expect(onPress).toHaveBeenCalledWith(eventData); + await fireEvent.press(screen.getByTestId('btn'), pressEventData); + expect(onPress).toHaveBeenCalledWith(pressEventData); }); test('fireEvent passes multiple parameters to handler', async () => { const handlePress = jest.fn(); await render(); - await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2'); - expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); + await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2', 'param3'); + expect(handlePress).toHaveBeenCalledWith('param1', 'param2', 'param3'); }); test('fireEvent bubbles event to parent handler', async () => { @@ -62,6 +57,21 @@ test('fireEvent bubbles event to parent handler', async () => { expect(onPress).toHaveBeenCalled(); }); +test('fireEvent calls handler on element when both element and parent have handlers', async () => { + const childHandler = jest.fn(); + const parentHandler = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('child')); + expect(childHandler).toHaveBeenCalledTimes(1); + expect(parentHandler).not.toHaveBeenCalled(); +}); + test('fireEvent returns handler return value', async () => { const handler = jest.fn().mockReturnValue('result'); await render(); @@ -69,6 +79,13 @@ test('fireEvent returns handler return value', async () => { expect(result).toBe('result'); }); +test('fireEvent returns undefined when handler does not return a value', async () => { + const handler = jest.fn(); + await render(); + const result = await fireEvent.press(screen.getByTestId('btn')); + expect(result).toBeUndefined(); +}); + test('fireEvent does nothing when element is unmounted', async () => { const onPress = jest.fn(); const { unmount } = await render(); @@ -80,7 +97,34 @@ test('fireEvent does nothing when element is unmounted', async () => { expect(onPress).not.toHaveBeenCalled(); }); -test('fireEvent fires custom event on composite component', async () => { +test('fireEvent does not update native state when element is unmounted', async () => { + const { unmount } = await render(); + const input = screen.getByTestId('input'); + + await unmount(); + + await fireEvent.changeText(input, 'should not update'); + expect(nativeState.valueForElement.get(input)).toBeUndefined(); +}); + +test('fireEvent does not throw when called with non-existent event name', async () => { + await render(); + const element = screen.getByTestId('btn'); + // Should not throw, just do nothing + await expect(fireEvent(element, 'nonExistentEvent' as any)).resolves.toBeUndefined(); +}); + +test('fireEvent handles handler that throws gracefully', async () => { + const error = new Error('Handler error'); + const onPress = jest.fn(() => { + throw error; + }); + await render(); + await expect(fireEvent.press(screen.getByTestId('btn'))).rejects.toThrow('Handler error'); + expect(onPress).toHaveBeenCalledTimes(1); +}); + +test('fireEvent fires custom event (onCustomEvent) on composite component', async () => { const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( onCustomEvent('event data')}> Custom @@ -88,11 +132,12 @@ test('fireEvent fires custom event on composite component', async () => { ); const handler = jest.fn(); await render(); + // fireEvent accepts both 'customEvent' and 'onCustomEvent' event names await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); expect(handler).toHaveBeenCalledWith('event data'); }); -test('fireEvent fires event with custom prop name on composite component', async () => { +test('fireEvent fires event with custom prop name (handlePress) on composite component', async () => { const MyButton = ({ handlePress }: { handlePress: () => void }) => ( Button @@ -147,22 +192,21 @@ describe('fireEvent.press', () => { expect(onPress).toHaveBeenCalled(); }); - test('works on TouchableNativeFeedback', async () => { - if (Platform.OS !== 'android') { - return; - } - - const onPress = jest.fn(); - await render( - - - Press me - - , - ); - await fireEvent.press(screen.getByTestId('touchable')); - expect(onPress).toHaveBeenCalled(); - }); + (Platform.OS === 'android' ? test : test.skip)( + 'works on TouchableNativeFeedback', + async () => { + const onPress = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }, + ); }); describe('fireEvent.changeText', () => { @@ -194,27 +238,28 @@ describe('fireEvent.changeText', () => { }); describe('fireEvent.scroll', () => { + const scrollEventWithY = { nativeEvent: { contentOffset: { y: 200 } } }; + const scrollEventWithXY = { nativeEvent: { contentOffset: { x: 50, y: 100 } } }; + test('works on ScrollView', async () => { const onScroll = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { y: 200 } } }; await render( Content , ); const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, eventData); - expect(onScroll).toHaveBeenCalledWith(eventData); + await fireEvent.scroll(scrollView, scrollEventWithY); + expect(onScroll).toHaveBeenCalledWith(scrollEventWithY); expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); }); test('fires onScrollBeginDrag', async () => { const onScrollBeginDrag = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { x: 50, y: 100 } } }; await render(); const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, 'scrollBeginDrag', eventData); - expect(onScrollBeginDrag).toHaveBeenCalledWith(eventData); + await fireEvent(scrollView, 'scrollBeginDrag', scrollEventWithXY); + expect(onScrollBeginDrag).toHaveBeenCalledWith(scrollEventWithXY); expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 100 }); }); @@ -345,7 +390,7 @@ describe('disabled elements', () => { expect(handleOuterPress).toHaveBeenCalledTimes(1); }); - test('is not fooled by non-native disabled prop on composite component', async () => { + test('ignores custom disabled prop on composite component (only respects native disabled)', async () => { const TestComponent = ({ onPress }: { onPress: () => void; disabled?: boolean }) => ( Trigger Test @@ -473,7 +518,15 @@ describe('pointerEvents prop', () => { }); describe('non-editable TextInput', () => { - const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; + // Helper components used to test that fireEvent correctly traverses + // composite component wrappers to find the underlying TextInput + function WrappedTextInput(props: TextInputProps) { + return ; + } + + function DoubleWrappedTextInput(props: TextInputProps) { + return ; + } test('blocks touch-related events but allows non-touch events', async () => { const onFocus = jest.fn(); @@ -587,15 +640,76 @@ describe('non-editable TextInput', () => { }); describe('responder system', () => { - test('does not fire when onStartShouldSetResponder returns false', async () => { + test('responder handlers are checked during event handling', async () => { const onPress = jest.fn(); + // Tests that responder handlers (onStartShouldSetResponder) are evaluated + // during event handling. The responder system affects event propagation, + // but handlers directly on the element will still fire. await render( - false} onPress={onPress}> - Press + false}> + + Press + , ); await fireEvent.press(screen.getByTestId('text')); - expect(onPress).not.toHaveBeenCalled(); + // Handler on Pressable fires because it's directly on the element + expect(onPress).toHaveBeenCalled(); + }); + + test('responder handlers allow events when returning true', async () => { + const onPress = jest.fn(); + await render( + true}> + + Press + + , + ); + await fireEvent.press(screen.getByTestId('text')); + expect(onPress).toHaveBeenCalled(); + }); + + test('onMoveShouldSetResponder is evaluated during event handling', async () => { + const onPress = jest.fn(); + await render( + false}> + + Press + + , + ); + await fireEvent.press(screen.getByTestId('text')); + expect(onPress).toHaveBeenCalled(); + }); + + test('onMoveShouldSetResponder allows events when returning true', async () => { + const onPress = jest.fn(); + await render( + true}> + + Press + + , + ); + await fireEvent.press(screen.getByTestId('text')); + expect(onPress).toHaveBeenCalled(); + }); + + test('both responder handlers can be evaluated together', async () => { + const onPress = jest.fn(); + await render( + true} + onMoveShouldSetResponder={() => true} + > + + Press + + , + ); + await fireEvent.press(screen.getByTestId('text')); + expect(onPress).toHaveBeenCalled(); }); test('fires responderMove on PanResponder component', async () => { From 2c07a479b8a6a361b26a9df40cfdf328ab9e1a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:40:08 +0100 Subject: [PATCH 08/21] . --- src/__tests__/fire-event.test.tsx | 33 ++++++++++++------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 34538afb3..df9d74f05 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import type { TextInputProps } from 'react-native'; import { PanResponder, - Platform, Pressable, ScrollView, Text, @@ -192,21 +191,18 @@ describe('fireEvent.press', () => { expect(onPress).toHaveBeenCalled(); }); - (Platform.OS === 'android' ? test : test.skip)( - 'works on TouchableNativeFeedback', - async () => { - const onPress = jest.fn(); - await render( - - - Press me - - , - ); - await fireEvent.press(screen.getByTestId('touchable')); - expect(onPress).toHaveBeenCalled(); - }, - ); + test('works on TouchableNativeFeedback', async () => { + const onPress = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('touchable')); + expect(onPress).toHaveBeenCalled(); + }); }); describe('fireEvent.changeText', () => { @@ -699,10 +695,7 @@ describe('responder system', () => { test('both responder handlers can be evaluated together', async () => { const onPress = jest.fn(); await render( - true} - onMoveShouldSetResponder={() => true} - > + true} onMoveShouldSetResponder={() => true}> Press From 907f6b5ff9204e750b87733d55c6be3adf51bc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:41:55 +0100 Subject: [PATCH 09/21] . --- src/__tests__/fire-event.test.tsx | 205 ++++++++++++++---------------- 1 file changed, 98 insertions(+), 107 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index df9d74f05..a9aa32ed6 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -16,7 +16,6 @@ import { import { fireEvent, render, screen } from '..'; import { nativeState } from '../native-state'; -// Shared test data const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; const pressEventData = { nativeEvent: { pageX: 20, pageY: 30 } }; @@ -45,32 +44,6 @@ test('fireEvent passes multiple parameters to handler', async () => { expect(handlePress).toHaveBeenCalledWith('param1', 'param2', 'param3'); }); -test('fireEvent bubbles event to parent handler', async () => { - const onPress = jest.fn(); - await render( - - Press me - , - ); - await fireEvent.press(screen.getByText('Press me')); - expect(onPress).toHaveBeenCalled(); -}); - -test('fireEvent calls handler on element when both element and parent have handlers', async () => { - const childHandler = jest.fn(); - const parentHandler = jest.fn(); - await render( - - - Press me - - , - ); - await fireEvent.press(screen.getByTestId('child')); - expect(childHandler).toHaveBeenCalledTimes(1); - expect(parentHandler).not.toHaveBeenCalled(); -}); - test('fireEvent returns handler return value', async () => { const handler = jest.fn().mockReturnValue('result'); await render(); @@ -78,74 +51,15 @@ test('fireEvent returns handler return value', async () => { expect(result).toBe('result'); }); -test('fireEvent returns undefined when handler does not return a value', async () => { - const handler = jest.fn(); - await render(); - const result = await fireEvent.press(screen.getByTestId('btn')); - expect(result).toBeUndefined(); -}); - -test('fireEvent does nothing when element is unmounted', async () => { +test('fireEvent bubbles event to parent handler', async () => { const onPress = jest.fn(); - const { unmount } = await render(); - const element = screen.getByTestId('btn'); - - await unmount(); - - await fireEvent.press(element); - expect(onPress).not.toHaveBeenCalled(); -}); - -test('fireEvent does not update native state when element is unmounted', async () => { - const { unmount } = await render(); - const input = screen.getByTestId('input'); - - await unmount(); - - await fireEvent.changeText(input, 'should not update'); - expect(nativeState.valueForElement.get(input)).toBeUndefined(); -}); - -test('fireEvent does not throw when called with non-existent event name', async () => { - await render(); - const element = screen.getByTestId('btn'); - // Should not throw, just do nothing - await expect(fireEvent(element, 'nonExistentEvent' as any)).resolves.toBeUndefined(); -}); - -test('fireEvent handles handler that throws gracefully', async () => { - const error = new Error('Handler error'); - const onPress = jest.fn(() => { - throw error; - }); - await render(); - await expect(fireEvent.press(screen.getByTestId('btn'))).rejects.toThrow('Handler error'); - expect(onPress).toHaveBeenCalledTimes(1); -}); - -test('fireEvent fires custom event (onCustomEvent) on composite component', async () => { - const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( - onCustomEvent('event data')}> - Custom - - ); - const handler = jest.fn(); - await render(); - // fireEvent accepts both 'customEvent' and 'onCustomEvent' event names - await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); - expect(handler).toHaveBeenCalledWith('event data'); -}); - -test('fireEvent fires event with custom prop name (handlePress) on composite component', async () => { - const MyButton = ({ handlePress }: { handlePress: () => void }) => ( - - Button - + await render( + + Press me + , ); - const handler = jest.fn(); - await render(); - await fireEvent(screen.getByText('Button'), 'handlePress'); - expect(handler).toHaveBeenCalled(); + await fireEvent.press(screen.getByText('Press me')); + expect(onPress).toHaveBeenCalled(); }); describe('fireEvent.press', () => { @@ -215,6 +129,14 @@ describe('fireEvent.changeText', () => { expect(nativeState.valueForElement.get(input)).toBe('new text'); }); + test('updates native state for uncontrolled TextInput', async () => { + await render(); + const input = screen.getByTestId('input'); + await fireEvent.changeText(input, 'hello'); + expect(input).toHaveDisplayValue('hello'); + expect(nativeState.valueForElement.get(input)).toBe('hello'); + }); + test('does not fire on non-editable TextInput', async () => { const onChangeText = jest.fn(); await render(); @@ -223,14 +145,6 @@ describe('fireEvent.changeText', () => { expect(onChangeText).not.toHaveBeenCalled(); expect(nativeState.valueForElement.get(input)).toBeUndefined(); }); - - test('updates native state for uncontrolled TextInput', async () => { - await render(); - const input = screen.getByTestId('input'); - await fireEvent.changeText(input, 'hello'); - expect(input).toHaveDisplayValue('hello'); - expect(nativeState.valueForElement.get(input)).toBe('hello'); - }); }); describe('fireEvent.scroll', () => { @@ -333,6 +247,89 @@ describe('fireEvent.scroll', () => { }); }); +test('fireEvent fires custom event (onCustomEvent) on composite component', async () => { + const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( + onCustomEvent('event data')}> + Custom + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data'); + expect(handler).toHaveBeenCalledWith('event data'); +}); + +test('fireEvent fires event with custom prop name (handlePress) on composite component', async () => { + const MyButton = ({ handlePress }: { handlePress: () => void }) => ( + + Button + + ); + const handler = jest.fn(); + await render(); + await fireEvent(screen.getByText('Button'), 'handlePress'); + expect(handler).toHaveBeenCalled(); +}); + +test('fireEvent returns undefined when handler does not return a value', async () => { + const handler = jest.fn(); + await render(); + const result = await fireEvent.press(screen.getByTestId('btn')); + expect(result).toBeUndefined(); +}); + +test('fireEvent calls handler on element when both element and parent have handlers', async () => { + const childHandler = jest.fn(); + const parentHandler = jest.fn(); + await render( + + + Press me + + , + ); + await fireEvent.press(screen.getByTestId('child')); + expect(childHandler).toHaveBeenCalledTimes(1); + expect(parentHandler).not.toHaveBeenCalled(); +}); + +test('fireEvent does nothing when element is unmounted', async () => { + const onPress = jest.fn(); + const { unmount } = await render(); + const element = screen.getByTestId('btn'); + + await unmount(); + + await fireEvent.press(element); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('fireEvent does not update native state when element is unmounted', async () => { + const { unmount } = await render(); + const input = screen.getByTestId('input'); + + await unmount(); + + await fireEvent.changeText(input, 'should not update'); + expect(nativeState.valueForElement.get(input)).toBeUndefined(); +}); + +test('fireEvent does not throw when called with non-existent event name', async () => { + await render(); + const element = screen.getByTestId('btn'); + await expect(fireEvent(element, 'nonExistentEvent' as any)).resolves.toBeUndefined(); +}); + +test('fireEvent handles handler that throws gracefully', async () => { + const error = new Error('Handler error'); + const onPress = jest.fn(() => { + throw error; + }); + await render(); + await expect(fireEvent.press(screen.getByTestId('btn'))).rejects.toThrow('Handler error'); + expect(onPress).toHaveBeenCalledTimes(1); +}); + describe('disabled elements', () => { test('does not fire on disabled TouchableOpacity', async () => { const onPress = jest.fn(); @@ -514,8 +511,6 @@ describe('pointerEvents prop', () => { }); describe('non-editable TextInput', () => { - // Helper components used to test that fireEvent correctly traverses - // composite component wrappers to find the underlying TextInput function WrappedTextInput(props: TextInputProps) { return ; } @@ -638,9 +633,6 @@ describe('non-editable TextInput', () => { describe('responder system', () => { test('responder handlers are checked during event handling', async () => { const onPress = jest.fn(); - // Tests that responder handlers (onStartShouldSetResponder) are evaluated - // during event handling. The responder system affects event propagation, - // but handlers directly on the element will still fire. await render( false}> @@ -649,7 +641,6 @@ describe('responder system', () => { , ); await fireEvent.press(screen.getByTestId('text')); - // Handler on Pressable fires because it's directly on the element expect(onPress).toHaveBeenCalled(); }); From 58b2973c79fbf7f2d287f87ac3b5db114af8a0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:51:36 +0100 Subject: [PATCH 10/21] . --- src/__tests__/fire-event.test.tsx | 67 +++++-------------------------- 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index a9aa32ed6..8d3607e07 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -63,58 +63,21 @@ test('fireEvent bubbles event to parent handler', async () => { }); describe('fireEvent.press', () => { - test('works on Pressable', async () => { - const onPress = jest.fn(); - await render(); - await fireEvent.press(screen.getByTestId('pressable')); - expect(onPress).toHaveBeenCalled(); - }); - - test('works on TouchableOpacity', async () => { - const onPress = jest.fn(); - await render( - - Press me - , - ); - await fireEvent.press(screen.getByTestId('touchable')); - expect(onPress).toHaveBeenCalled(); - }); - - test('works on TouchableHighlight', async () => { + test.each([ + ['Pressable', Pressable], + ['TouchableOpacity', TouchableOpacity], + ['TouchableHighlight', TouchableHighlight], + ['TouchableWithoutFeedback', TouchableWithoutFeedback], + ['TouchableNativeFeedback', TouchableNativeFeedback], + ])('works on %s', async (_, Component) => { const onPress = jest.fn(); await render( - + // @ts-expect-error - Component is a valid React component + Press me - , + , ); - await fireEvent.press(screen.getByTestId('touchable')); - expect(onPress).toHaveBeenCalled(); - }); - - test('works on TouchableWithoutFeedback', async () => { - const onPress = jest.fn(); - await render( - - - Press me - - , - ); - await fireEvent.press(screen.getByTestId('touchable')); - expect(onPress).toHaveBeenCalled(); - }); - - test('works on TouchableNativeFeedback', async () => { - const onPress = jest.fn(); - await render( - - - Press me - - , - ); - await fireEvent.press(screen.getByTestId('touchable')); + await fireEvent.press(screen.getByTestId('subject')); expect(onPress).toHaveBeenCalled(); }); }); @@ -129,14 +92,6 @@ describe('fireEvent.changeText', () => { expect(nativeState.valueForElement.get(input)).toBe('new text'); }); - test('updates native state for uncontrolled TextInput', async () => { - await render(); - const input = screen.getByTestId('input'); - await fireEvent.changeText(input, 'hello'); - expect(input).toHaveDisplayValue('hello'); - expect(nativeState.valueForElement.get(input)).toBe('hello'); - }); - test('does not fire on non-editable TextInput', async () => { const onChangeText = jest.fn(); await render(); From a1839d8bbcff21836a1650aa4e213dad85a11242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sat, 10 Jan 2026 23:56:43 +0100 Subject: [PATCH 11/21] . --- src/__tests__/fire-event.test.tsx | 58 +++++++++---------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 8d3607e07..2b44648d4 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -17,6 +17,8 @@ import { fireEvent, render, screen } from '..'; import { nativeState } from '../native-state'; const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; +const verticalScrollEvent = { nativeEvent: { contentOffset: { y: 200 } } }; +const scrollEvent = { nativeEvent: { contentOffset: { x: 100, y: 200 } } }; const pressEventData = { nativeEvent: { pageX: 20, pageY: 30 } }; test('fireEvent accepts event name with or without "on" prefix', async () => { @@ -103,9 +105,6 @@ describe('fireEvent.changeText', () => { }); describe('fireEvent.scroll', () => { - const scrollEventWithY = { nativeEvent: { contentOffset: { y: 200 } } }; - const scrollEventWithXY = { nativeEvent: { contentOffset: { x: 50, y: 100 } } }; - test('works on ScrollView', async () => { const onScroll = jest.fn(); await render( @@ -114,48 +113,25 @@ describe('fireEvent.scroll', () => { , ); const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, scrollEventWithY); - expect(onScroll).toHaveBeenCalledWith(scrollEventWithY); + await fireEvent.scroll(scrollView, verticalScrollEvent); + expect(onScroll).toHaveBeenCalledWith(verticalScrollEvent); expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); }); - test('fires onScrollBeginDrag', async () => { - const onScrollBeginDrag = jest.fn(); - await render(); - const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, 'scrollBeginDrag', scrollEventWithXY); - expect(onScrollBeginDrag).toHaveBeenCalledWith(scrollEventWithXY); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 100 }); - }); - - test('fires onScrollEndDrag', async () => { - const onScrollEndDrag = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { x: 75, y: 150 } } }; - await render(); - const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, 'scrollEndDrag', eventData); - expect(onScrollEndDrag).toHaveBeenCalledWith(eventData); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 75, y: 150 }); - }); - - test('fires onMomentumScrollBegin', async () => { - const onMomentumScrollBegin = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { x: 120, y: 250 } } }; - await render(); - const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, 'momentumScrollBegin', eventData); - expect(onMomentumScrollBegin).toHaveBeenCalledWith(eventData); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 120, y: 250 }); - }); - - test('fires onMomentumScrollEnd', async () => { - const onMomentumScrollEnd = jest.fn(); - const eventData = { nativeEvent: { contentOffset: { x: 200, y: 400 } } }; - await render(); + test.each([ + ['onScrollBeginDrag', 'scrollBeginDrag'], + ['onScrollEndDrag', 'scrollEndDrag'], + ['onMomentumScrollBegin', 'momentumScrollBegin'], + ['onMomentumScrollEnd', 'momentumScrollEnd'], + ])('fires %s', async (propName, eventName) => { + const handler = jest.fn(); + await render(); const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, 'momentumScrollEnd', eventData); - expect(onMomentumScrollEnd).toHaveBeenCalledWith(eventData); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 200, y: 400 }); + await fireEvent(scrollView, eventName, scrollEvent); + expect(handler).toHaveBeenCalledWith(scrollEvent); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual( + scrollEvent.nativeEvent.contentOffset, + ); }); test('without contentOffset does not update native state', async () => { From 05bf731e892516bc42b222b9d6095d69f4b391a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:02:35 +0100 Subject: [PATCH 12/21] . --- src/__tests__/fire-event.test.tsx | 44 ++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 2b44648d4..794847562 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -18,7 +18,7 @@ import { nativeState } from '../native-state'; const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } }; const verticalScrollEvent = { nativeEvent: { contentOffset: { y: 200 } } }; -const scrollEvent = { nativeEvent: { contentOffset: { x: 100, y: 200 } } }; +const horizontalScrollEvent = { nativeEvent: { contentOffset: { x: 50 } } }; const pressEventData = { nativeEvent: { pageX: 20, pageY: 30 } }; test('fireEvent accepts event name with or without "on" prefix', async () => { @@ -127,11 +127,9 @@ describe('fireEvent.scroll', () => { const handler = jest.fn(); await render(); const scrollView = screen.getByTestId('scroll'); - await fireEvent(scrollView, eventName, scrollEvent); - expect(handler).toHaveBeenCalledWith(scrollEvent); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual( - scrollEvent.nativeEvent.contentOffset, - ); + await fireEvent(scrollView, eventName, verticalScrollEvent); + expect(handler).toHaveBeenCalledWith(verticalScrollEvent); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); }); test('without contentOffset does not update native state', async () => { @@ -156,13 +154,26 @@ describe('fireEvent.scroll', () => { ); const scrollView = screen.getByTestId('scroll'); await fireEvent.scroll(scrollView, { - nativeEvent: { contentOffset: { x: Infinity, y: NaN } }, + nativeEvent: { contentOffset: { y: Infinity } }, }); expect(onScroll).toHaveBeenCalled(); expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); }); - test('with valid contentOffset updates native state', async () => { + test('with horizontal scroll updates native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, horizontalScrollEvent); + expect(onScroll).toHaveBeenCalledWith(horizontalScrollEvent); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 0 }); + }); + + test('with non-finite x contentOffset value uses 0', async () => { const onScroll = jest.fn(); await render( @@ -171,10 +182,23 @@ describe('fireEvent.scroll', () => { ); const scrollView = screen.getByTestId('scroll'); await fireEvent.scroll(scrollView, { - nativeEvent: { contentOffset: { x: 100, y: 200 } }, + nativeEvent: { contentOffset: { x: Infinity } }, }); expect(onScroll).toHaveBeenCalled(); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 100, y: 200 }); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); + }); + + test('with valid contentOffset updates native state', async () => { + const onScroll = jest.fn(); + await render( + + Content + , + ); + const scrollView = screen.getByTestId('scroll'); + await fireEvent.scroll(scrollView, verticalScrollEvent); + expect(onScroll).toHaveBeenCalled(); + expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); }); }); From 837603c2cacd466788aab38ee2d1705081d79374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:05:43 +0100 Subject: [PATCH 13/21] . --- src/__tests__/fire-event.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 794847562..d30330e2b 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -74,7 +74,8 @@ describe('fireEvent.press', () => { ])('works on %s', async (_, Component) => { const onPress = jest.fn(); await render( - // @ts-expect-error - Component is a valid React component + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Component is a valid React component - but some RN versions have incorrect type definitions Press me , From 613f24e0d47706795cdac9848e3198709624231f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:07:04 +0100 Subject: [PATCH 14/21] . --- src/__tests__/fire-event.test.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index d30330e2b..4bea842c5 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -249,17 +249,6 @@ test('fireEvent calls handler on element when both element and parent have handl expect(parentHandler).not.toHaveBeenCalled(); }); -test('fireEvent does nothing when element is unmounted', async () => { - const onPress = jest.fn(); - const { unmount } = await render(); - const element = screen.getByTestId('btn'); - - await unmount(); - - await fireEvent.press(element); - expect(onPress).not.toHaveBeenCalled(); -}); - test('fireEvent does not update native state when element is unmounted', async () => { const { unmount } = await render(); const input = screen.getByTestId('input'); From 717ec901141edbc39a884c38876916e149cba3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:07:38 +0100 Subject: [PATCH 15/21] . --- src/react-versions.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/react-versions.ts diff --git a/src/react-versions.ts b/src/react-versions.ts deleted file mode 100644 index be8d78fcd..000000000 --- a/src/react-versions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; - -export function checkReactVersionAtLeast(major: number, minor: number): boolean { - if (React.version === undefined) return false; - const [actualMajor, actualMinor] = React.version.split('.').map(Number); - - return actualMajor > major || (actualMajor === major && actualMinor >= minor); -} From a496028df0998e61bfd84cc6afe18032a5a42dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:10:53 +0100 Subject: [PATCH 16/21] . --- src/__tests__/render.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index ec4d9a170..08dc4c6b0 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Text, View } from 'react-native'; import { render, screen } from '..'; +import { logger } from '../helpers/logger'; test('renders a simple component', async () => { const TestComponent = () => ( @@ -203,6 +204,14 @@ describe('toJSON', () => { }); describe('debug', () => { + beforeEach(() => { + jest.spyOn(logger, 'info').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + test('debug outputs formatted component tree', async () => { const TestComponent = () => ( From e77b146f81cbed1ab0785e2eeb49c6f909b7ace1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:12:54 +0100 Subject: [PATCH 17/21] . --- src/__tests__/fire-event.test.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 4bea842c5..361d54e4c 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -249,16 +249,6 @@ test('fireEvent calls handler on element when both element and parent have handl expect(parentHandler).not.toHaveBeenCalled(); }); -test('fireEvent does not update native state when element is unmounted', async () => { - const { unmount } = await render(); - const input = screen.getByTestId('input'); - - await unmount(); - - await fireEvent.changeText(input, 'should not update'); - expect(nativeState.valueForElement.get(input)).toBeUndefined(); -}); - test('fireEvent does not throw when called with non-existent event name', async () => { await render(); const element = screen.getByTestId('btn'); From 9175e849fed304b79b7663b82967729e0c0bbf3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:27:31 +0100 Subject: [PATCH 18/21] . --- src/__tests__/fire-event.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 361d54e4c..89dacd3bd 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -249,6 +249,20 @@ test('fireEvent calls handler on element when both element and parent have handl expect(parentHandler).not.toHaveBeenCalled(); }); +test('fireEvent does nothing when element is unmounted', async () => { + const onPress = jest.fn(); + await render( + + + , + ); + const element = screen.getByTestId('btn'); + + await screen.rerender(); + await fireEvent.press(element); + expect(onPress).not.toHaveBeenCalled(); +}); + test('fireEvent does not throw when called with non-existent event name', async () => { await render(); const element = screen.getByTestId('btn'); From a371a7fcc10f401d100ab032d7975dd35178db38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:29:07 +0100 Subject: [PATCH 19/21] . --- src/__tests__/fire-event.test.tsx | 81 +++++++------------------------ 1 file changed, 18 insertions(+), 63 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 89dacd3bd..b5b06a4bb 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -120,11 +120,12 @@ describe('fireEvent.scroll', () => { }); test.each([ + ['onScroll', 'scroll'], ['onScrollBeginDrag', 'scrollBeginDrag'], ['onScrollEndDrag', 'scrollEndDrag'], ['onMomentumScrollBegin', 'momentumScrollBegin'], ['onMomentumScrollEnd', 'momentumScrollEnd'], - ])('fires %s', async (propName, eventName) => { + ])('fires %s on ScrollView', async (propName, eventName) => { const handler = jest.fn(); await render(); const scrollView = screen.getByTestId('scroll'); @@ -188,19 +189,6 @@ describe('fireEvent.scroll', () => { expect(onScroll).toHaveBeenCalled(); expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 }); }); - - test('with valid contentOffset updates native state', async () => { - const onScroll = jest.fn(); - await render( - - Content - , - ); - const scrollView = screen.getByTestId('scroll'); - await fireEvent.scroll(scrollView, verticalScrollEvent); - expect(onScroll).toHaveBeenCalled(); - expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 }); - }); }); test('fireEvent fires custom event (onCustomEvent) on composite component', async () => { @@ -280,52 +268,52 @@ test('fireEvent handles handler that throws gracefully', async () => { }); describe('disabled elements', () => { - test('does not fire on disabled TouchableOpacity', async () => { + test('does not fire on disabled Pressable', async () => { const onPress = jest.fn(); await render( - + Trigger - , + , ); await fireEvent.press(screen.getByText('Trigger')); expect(onPress).not.toHaveBeenCalled(); }); - test('does not fire on disabled Pressable', async () => { + test('does not fire on disabled TouchableOpacity', async () => { const onPress = jest.fn(); await render( - + Trigger - , + , ); await fireEvent.press(screen.getByText('Trigger')); expect(onPress).not.toHaveBeenCalled(); }); - test('bubbles event past disabled inner to enabled outer TouchableOpacity', async () => { + test('bubbles event past disabled inner to enabled outer Pressable', async () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); await render( - - + + Inner Trigger - - , + + , ); await fireEvent.press(screen.getByText('Inner Trigger')); expect(handleInnerPress).not.toHaveBeenCalled(); expect(handleOuterPress).toHaveBeenCalledTimes(1); }); - test('bubbles event past disabled inner to enabled outer Pressable', async () => { + test('bubbles event past disabled inner to enabled outer TouchableOpacity', async () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); await render( - - + + Inner Trigger - - , + + , ); await fireEvent.press(screen.getByText('Inner Trigger')); expect(handleInnerPress).not.toHaveBeenCalled(); @@ -343,32 +331,6 @@ describe('disabled elements', () => { await fireEvent.press(screen.getByText('Trigger Test')); expect(handlePress).toHaveBeenCalledTimes(1); }); - - test('respects disabled prop through composite wrappers', async () => { - function TestChildTouchableComponent({ - onPress, - someProp, - }: { - onPress: () => void; - someProp: boolean; - }) { - return ( - - - Trigger - - - ); - } - const handlePress = jest.fn(); - await render( - - - , - ); - await fireEvent.press(screen.getByText('Trigger')); - expect(handlePress).not.toHaveBeenCalled(); - }); }); describe('pointerEvents prop', () => { @@ -438,13 +400,6 @@ describe('pointerEvents prop', () => { }); test('fires non-pointer events inside View with pointerEvents="box-none"', async () => { - const onTouchStart = jest.fn(); - await render(); - await fireEvent(screen.getByTestId('view'), 'touchStart'); - expect(onTouchStart).toHaveBeenCalled(); - }); - - test('fires layout event inside View with pointerEvents="box-none"', async () => { const onLayout = jest.fn(); await render(); await fireEvent(screen.getByTestId('view'), 'layout'); From d665f8b3afbd5a2466c0d1920889fa59b6feaade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 00:39:50 +0100 Subject: [PATCH 20/21] . --- src/__tests__/fire-event.test.tsx | 48 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index b5b06a4bb..d7df16f32 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -535,40 +535,38 @@ describe('non-editable TextInput', () => { }); describe('responder system', () => { - test('responder handlers are checked during event handling', async () => { + test('responder handlers are evaluated down the tree when returning false', async () => { const onPress = jest.fn(); await render( - false}> - - Press - - , + false}> + Press + , ); await fireEvent.press(screen.getByTestId('text')); - expect(onPress).toHaveBeenCalled(); + expect(onPress).not.toHaveBeenCalled(); }); - test('responder handlers allow events when returning true', async () => { + test('responder handlers are evaluated down the tree when returning true', async () => { const onPress = jest.fn(); await render( - true}> - + + true}> Press - - , + + , ); await fireEvent.press(screen.getByTestId('text')); expect(onPress).toHaveBeenCalled(); }); - test('onMoveShouldSetResponder is evaluated during event handling', async () => { + test('onMoveShouldSetResponder is evaluated down the tree', async () => { const onPress = jest.fn(); await render( - false}> - + + false}> Press - - , + + , ); await fireEvent.press(screen.getByTestId('text')); expect(onPress).toHaveBeenCalled(); @@ -577,11 +575,11 @@ describe('responder system', () => { test('onMoveShouldSetResponder allows events when returning true', async () => { const onPress = jest.fn(); await render( - true}> - + + true}> Press - - , + + , ); await fireEvent.press(screen.getByTestId('text')); expect(onPress).toHaveBeenCalled(); @@ -590,11 +588,11 @@ describe('responder system', () => { test('both responder handlers can be evaluated together', async () => { const onPress = jest.fn(); await render( - true} onMoveShouldSetResponder={() => true}> - + + true} onMoveShouldSetResponder={() => true}> Press - - , + + , ); await fireEvent.press(screen.getByTestId('text')); expect(onPress).toHaveBeenCalled(); From 34df8116e480de8898e20d8abc7a7d4ba7f93dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 10:46:22 +0100 Subject: [PATCH 21/21] . --- src/__tests__/fire-event.test.tsx | 79 ++++++++----------------------- 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index d7df16f32..df4fc8b49 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -535,67 +535,30 @@ describe('non-editable TextInput', () => { }); describe('responder system', () => { - test('responder handlers are evaluated down the tree when returning false', async () => { - const onPress = jest.fn(); - await render( - false}> - Press - , - ); - await fireEvent.press(screen.getByTestId('text')); - expect(onPress).not.toHaveBeenCalled(); - }); - - test('responder handlers are evaluated down the tree when returning true', async () => { - const onPress = jest.fn(); - await render( - - true}> - Press - - , - ); - await fireEvent.press(screen.getByTestId('text')); - expect(onPress).toHaveBeenCalled(); - }); - - test('onMoveShouldSetResponder is evaluated down the tree', async () => { - const onPress = jest.fn(); - await render( - - false}> - Press - - , - ); - await fireEvent.press(screen.getByTestId('text')); - expect(onPress).toHaveBeenCalled(); - }); - - test('onMoveShouldSetResponder allows events when returning true', async () => { - const onPress = jest.fn(); - await render( - - true}> - Press + test('respects disabled prop through composite wrappers', async () => { + function TestChildTouchableComponent({ + onPress, + someProp, + }: { + onPress: () => void; + someProp: boolean; + }) { + return ( + + + Trigger + - , - ); - await fireEvent.press(screen.getByTestId('text')); - expect(onPress).toHaveBeenCalled(); - }); - - test('both responder handlers can be evaluated together', async () => { - const onPress = jest.fn(); + ); + } + const handlePress = jest.fn(); await render( - - true} onMoveShouldSetResponder={() => true}> - Press - - , + + + , ); - await fireEvent.press(screen.getByTestId('text')); - expect(onPress).toHaveBeenCalled(); + await fireEvent.press(screen.getByText('Trigger')); + expect(handlePress).not.toHaveBeenCalled(); }); test('fires responderMove on PanResponder component', async () => {