diff --git a/src/__tests__/fire-event-textInput.test.tsx b/src/__tests__/fire-event-textInput.test.tsx
deleted file mode 100644
index 7223b1b2..00000000
--- 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 96566350..df4fc8b4 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/__tests__/fire-event.test.tsx
@@ -1,659 +1,583 @@
import * as React from 'react';
+import type { TextInputProps } from 'react-native';
import {
PanResponder,
Pressable,
ScrollView,
Text,
TextInput,
+ TouchableHighlight,
+ TouchableNativeFeedback,
TouchableOpacity,
+ TouchableWithoutFeedback,
View,
} from 'react-native';
-import { fireEvent, render, screen, waitFor } from '..';
+import { fireEvent, render, screen } from '..';
+import { nativeState } from '../native-state';
-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();
- });
+const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } };
+const verticalScrollEvent = { nativeEvent: { contentOffset: { y: 200 } } };
+const horizontalScrollEvent = { nativeEvent: { contentOffset: { x: 50 } } };
+const pressEventData = { nativeEvent: { pageX: 20, pageY: 30 } };
- 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(
-
-
- ,
- );
+test('fireEvent accepts event name with or without "on" prefix', async () => {
+ const onPress = jest.fn();
+ await render();
- await fireEvent(screen.getByText('Custom event component'), 'customEvent', EVENT_DATA);
+ await fireEvent(screen.getByTestId('btn'), 'press');
+ expect(onPress).toHaveBeenCalledTimes(1);
- expect(handlerMock).toHaveBeenCalledWith(EVENT_DATA);
- });
+ await fireEvent(screen.getByTestId('btn'), 'onPress');
+ expect(onPress).toHaveBeenCalledTimes(2);
});
-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 passes event data to handler', async () => {
+ const onPress = jest.fn();
+ await render();
+ await fireEvent.press(screen.getByTestId('btn'), pressEventData);
+ expect(onPress).toHaveBeenCalledWith(pressEventData);
});
-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 passes multiple parameters to handler', async () => {
+ const handlePress = jest.fn();
+ await render();
+ await fireEvent(screen.getByTestId('btn'), 'press', 'param1', 'param2', 'param3');
+ expect(handlePress).toHaveBeenCalledWith('param1', 'param2', 'param3');
});
-test('fireEvent.changeText', async () => {
- const onChangeTextMock = jest.fn();
+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 bubbles event to parent handler', async () => {
+ const onPress = jest.fn();
await render(
-
-
- ,
+
+ Press me
+ ,
);
-
- const input = screen.getByPlaceholderText('Customer placeholder');
- await fireEvent.changeText(input, 'content');
- expect(onChangeTextMock).toHaveBeenCalledWith('content');
+ await fireEvent.press(screen.getByText('Press me'));
+ expect(onPress).toHaveBeenCalled();
});
-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');
+describe('fireEvent.press', () => {
+ test.each([
+ ['Pressable', Pressable],
+ ['TouchableOpacity', TouchableOpacity],
+ ['TouchableHighlight', TouchableHighlight],
+ ['TouchableWithoutFeedback', TouchableWithoutFeedback],
+ ['TouchableNativeFeedback', TouchableNativeFeedback],
+ ])('works on %s', async (_, Component) => {
+ const onPress = jest.fn();
+ await render(
+ // 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
+ ,
+ );
+ await fireEvent.press(screen.getByTestId('subject'));
+ expect(onPress).toHaveBeenCalled();
+ });
});
-test('custom component with custom event name', async () => {
- const handlePress = jest.fn();
-
- await render();
-
- await fireEvent(screen.getByText('Custom component'), 'handlePress');
+describe('fireEvent.changeText', () => {
+ test('works on TextInput', async () => {
+ const onChangeText = jest.fn();
+ await render();
+ const input = screen.getByTestId('input');
+ await fireEvent.changeText(input, 'new text');
+ expect(onChangeText).toHaveBeenCalledWith('new text');
+ expect(nativeState.valueForElement.get(input)).toBe('new text');
+ });
- expect(handlePress).toHaveBeenCalled();
+ test('does not fire on non-editable TextInput', async () => {
+ const onChangeText = jest.fn();
+ await render();
+ const input = screen.getByTestId('input');
+ await fireEvent.changeText(input, 'new text');
+ expect(onChangeText).not.toHaveBeenCalled();
+ expect(nativeState.valueForElement.get(input)).toBeUndefined();
+ });
});
-test('event with multiple handler parameters', async () => {
- const handlePress = jest.fn();
-
- await render();
-
- await fireEvent(screen.getByText('Custom component'), 'handlePress', 'param1', 'param2');
+describe('fireEvent.scroll', () => {
+ test('works on ScrollView', async () => {
+ const onScroll = jest.fn();
+ await render(
+
+ Content
+ ,
+ );
+ const scrollView = screen.getByTestId('scroll');
+ await fireEvent.scroll(scrollView, verticalScrollEvent);
+ expect(onScroll).toHaveBeenCalledWith(verticalScrollEvent);
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
+ });
- expect(handlePress).toHaveBeenCalledWith('param1', 'param2');
-});
+ test.each([
+ ['onScroll', 'scroll'],
+ ['onScrollBeginDrag', 'scrollBeginDrag'],
+ ['onScrollEndDrag', 'scrollEndDrag'],
+ ['onMomentumScrollBegin', 'momentumScrollBegin'],
+ ['onMomentumScrollEnd', 'momentumScrollEnd'],
+ ])('fires %s on ScrollView', async (propName, eventName) => {
+ const handler = jest.fn();
+ await render();
+ const scrollView = screen.getByTestId('scroll');
+ await fireEvent(scrollView, eventName, verticalScrollEvent);
+ expect(handler).toHaveBeenCalledWith(verticalScrollEvent);
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
+ });
-test('should not fire on disabled TouchableOpacity', async () => {
- const handlePress = jest.fn();
- await render(
-
-
- Trigger
-
- ,
- );
+ test('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();
+ });
- await fireEvent.press(screen.getByText('Trigger'));
- expect(handlePress).not.toHaveBeenCalled();
-});
+ 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: { y: Infinity } },
+ });
+ expect(onScroll).toHaveBeenCalled();
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
+ });
-test('should not fire on disabled Pressable', async () => {
- const handlePress = jest.fn();
- await render(
-
-
- Trigger
-
- ,
- );
+ 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 });
+ });
- await fireEvent.press(screen.getByText('Trigger'));
- expect(handlePress).not.toHaveBeenCalled();
+ test('with non-finite x contentOffset value uses 0', async () => {
+ const onScroll = jest.fn();
+ await render(
+
+ Content
+ ,
+ );
+ const scrollView = screen.getByTestId('scroll');
+ await fireEvent.scroll(scrollView, {
+ nativeEvent: { contentOffset: { x: Infinity } },
+ });
+ expect(onScroll).toHaveBeenCalled();
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
+ });
});
-test('should not fire inside View with pointerEvents="none"', async () => {
- const onPress = jest.fn();
- await render(
-
-
- Trigger
-
- ,
+test('fireEvent fires custom event (onCustomEvent) on composite component', async () => {
+ const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => (
+ onCustomEvent('event data')}>
+ Custom
+
);
-
- await fireEvent.press(screen.getByText('Trigger'));
- await fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
+ const handler = jest.fn();
+ await render();
+ await fireEvent(screen.getByText('Custom'), 'customEvent', 'event data');
+ expect(handler).toHaveBeenCalledWith('event data');
});
-test('should not fire inside View with pointerEvents="box-only"', async () => {
- const onPress = jest.fn();
- await render(
-
-
- Trigger
-
- ,
+test('fireEvent fires event with custom prop name (handlePress) on composite component', async () => {
+ const MyButton = ({ handlePress }: { handlePress: () => void }) => (
+
+ Button
+
);
-
- await fireEvent.press(screen.getByText('Trigger'));
- await fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).not.toHaveBeenCalled();
+ const handler = jest.fn();
+ await render();
+ await fireEvent(screen.getByText('Button'), 'handlePress');
+ expect(handler).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('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('should fire inside View with pointerEvents="auto"', async () => {
- const onPress = jest.fn();
+test('fireEvent calls handler on element when both element and parent have handlers', async () => {
+ const childHandler = jest.fn();
+ const parentHandler = jest.fn();
await render(
-
-
- Trigger
+
+
+ Press me
- ,
+ ,
);
-
- await fireEvent.press(screen.getByText('Trigger'));
- await fireEvent(screen.getByText('Trigger'), 'onPress');
- expect(onPress).toHaveBeenCalledTimes(2);
+ await fireEvent.press(screen.getByTestId('child'));
+ expect(childHandler).toHaveBeenCalledTimes(1);
+ expect(parentHandler).not.toHaveBeenCalled();
});
-test('should not fire deeply inside View with pointerEvents="box-only"', async () => {
+test('fireEvent does nothing when element is unmounted', async () => {
const onPress = jest.fn();
await render(
-
-
-
- Trigger
-
-
+
+
,
);
+ const element = screen.getByTestId('btn');
- await fireEvent.press(screen.getByText('Trigger'));
- await fireEvent(screen.getByText('Trigger'), 'onPress');
+ await screen.rerender();
+ await fireEvent.press(element);
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('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('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);
+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);
});
-type TestChildTouchableComponentProps = {
- onPress: () => void;
- someProp: boolean;
-};
-
-function TestChildTouchableComponent({ onPress, someProp }: TestChildTouchableComponentProps) {
- return (
-
-
+describe('disabled elements', () => {
+ test('does not fire on disabled Pressable', async () => {
+ const onPress = jest.fn();
+ await render(
+
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;
+ ,
+ );
+ await fireEvent.press(screen.getByText('Trigger'));
+ expect(onPress).not.toHaveBeenCalled();
+ });
- return (
-
- Trigger
-
- );
-}
+ 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('has only onMove', async () => {
- const handleDrag = jest.fn();
+ 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);
+ });
- await render();
+ 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);
+ });
- await fireEvent(screen.getByText('Trigger'), 'responderMove', {
- touchHistory: { mostRecentTimeStamp: '2', touchBank: [] },
+ test('ignores custom disabled prop on composite component (only respects native disabled)', 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);
});
- 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();
+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('triggers onScrollEndDrag', async () => {
- const onScrollEndDragSpy = jest.fn();
- await render();
-
- await fireEvent(screen.getByTestId('test-id'), 'onScrollEndDrag');
- expect(onScrollEndDragSpy).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('triggers onMomentumScrollBegin', async () => {
- const onMomentumScrollBeginSpy = jest.fn();
- await render();
-
- await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollBegin');
- expect(onMomentumScrollBeginSpy).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('triggers onMomentumScrollEnd', async () => {
- const onMomentumScrollEndSpy = jest.fn();
- await render();
-
- await fireEvent(screen.getByTestId('test-id'), 'onMomentumScrollEnd');
- expect(onMomentumScrollEndSpy).toHaveBeenCalled();
+ 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);
});
-});
-describe('React.Suspense integration', () => {
- let mockPromise: Promise;
- let resolveMockPromise: (value: string) => void;
-
- beforeEach(() => {
- mockPromise = new Promise((resolve) => {
- resolveMockPromise = resolve;
- });
+ 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();
});
- type AsyncComponentProps = {
- onPress: () => void;
- shouldSuspend: boolean;
- };
+ test('fires non-pointer events inside View with pointerEvents="box-none"', async () => {
+ const onLayout = jest.fn();
+ await render();
+ await fireEvent(screen.getByTestId('view'), 'layout');
+ expect(onLayout).toHaveBeenCalled();
+ });
- function AsyncComponent({ onPress, shouldSuspend }: AsyncComponentProps) {
- if (shouldSuspend) {
- throw mockPromise;
- }
+ 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();
+ });
+});
- return (
-
- Async Component Loaded
-
- );
+describe('non-editable TextInput', () => {
+ function WrappedTextInput(props: TextInputProps) {
+ return ;
}
- function SuspenseWrapper({ children }: { children: React.ReactNode }) {
- return Loading...}>{children};
+ function DoubleWrappedTextInput(props: TextInputProps) {
+ return ;
}
- test('should handle events after Suspense resolves', async () => {
- const onPressMock = jest.fn();
+ 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(
-
-
- ,
+ ,
);
- // 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();
- });
+ 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);
- // fireEvent should work on the resolved component
- await fireEvent.press(screen.getByText('Async Component Loaded'));
- expect(onPressMock).toHaveBeenCalled();
+ expect(onFocus).not.toHaveBeenCalled();
+ expect(onChangeText).not.toHaveBeenCalled();
+ expect(onSubmitEditing).not.toHaveBeenCalled();
+ expect(onLayout).toHaveBeenCalledWith(layoutEvent);
});
- test('should handle events on Suspense fallback components', async () => {
- const fallbackPressMock = jest.fn();
-
- function InteractiveFallback() {
- return (
-
- Loading with button...
-
- );
- }
+ 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
+ ,
);
- // 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();
+ 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('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;
- }
+ 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();
- return (
-
- {level} Component Loaded
-
- );
- }
-
- const { rerender } = await render(
- Outer Loading...}>
-
- Inner Loading...}>
-
-
- ,
+ await render(
+ ,
);
- // 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...}>
-
-
- ,
- );
- });
+ 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);
- // 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();
+ expect(onFocus).not.toHaveBeenCalled();
+ expect(onChangeText).not.toHaveBeenCalled();
+ expect(onSubmitEditing).not.toHaveBeenCalled();
+ expect(onLayout).toHaveBeenCalledWith(layoutEvent);
});
- test('should work when events cause components to suspend', async () => {
- const onPressMock = jest.fn();
- let shouldSuspend = false;
+ test('fires layout event', async () => {
+ const onLayout = jest.fn();
+ await render();
+ await fireEvent(screen.getByTestId('input'), 'layout');
+ expect(onLayout).toHaveBeenCalled();
+ });
- function DataComponent() {
- if (shouldSuspend) {
- throw mockPromise; // This will cause suspense
- }
- return Data loaded;
- }
+ test('fires scroll event', async () => {
+ const onScroll = jest.fn();
+ await render();
+ await fireEvent(screen.getByTestId('input'), 'scroll');
+ expect(onScroll).toHaveBeenCalled();
+ });
+});
- function ButtonComponent() {
+describe('responder system', () => {
+ test('respects disabled prop through composite wrappers', async () => {
+ function TestChildTouchableComponent({
+ onPress,
+ someProp,
+ }: {
+ onPress: () => void;
+ someProp: boolean;
+ }) {
return (
- {
- onPressMock();
- shouldSuspend = true; // This will cause DataComponent to suspend on next render
- }}
- >
- Load Data
-
+
+
+ Trigger
+
+
);
}
-
+ const handlePress = jest.fn();
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();
+ await fireEvent.press(screen.getByText('Trigger'));
+ expect(handlePress).not.toHaveBeenCalled();
});
-});
-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();
+ 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();
+ });
});
diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx
index ec4d9a17..08dc4c6b 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 = () => (
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 96276643..0a1a176f 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,
diff --git a/src/react-versions.ts b/src/react-versions.ts
deleted file mode 100644
index be8d78fc..00000000
--- 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);
-}