diff --git a/src/__tests__/fireEvent-textInput.test.tsx b/src/__tests__/fireEvent-textInput.test.tsx
new file mode 100644
index 000000000..f36a68f4b
--- /dev/null
+++ b/src/__tests__/fireEvent-textInput.test.tsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import { Text, TextInput, TextInputProps } from 'react-native';
+import { render, fireEvent } 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', () => {
+ const onFocus = jest.fn();
+ const onChangeText = jest.fn();
+ const onSubmitEditing = jest.fn();
+ const onLayout = jest.fn();
+
+ const view = render(
+
+ );
+
+ const subject = view.getByTestId('subject');
+ fireEvent(subject, 'focus');
+ fireEvent.changeText(subject, 'Text');
+ fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ 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', () => {
+ const onFocus = jest.fn();
+ const onChangeText = jest.fn();
+ const onSubmitEditing = jest.fn();
+ const onLayout = jest.fn();
+
+ const view = render(
+
+ Nested Text
+
+ );
+
+ const subject = view.getByText('Nested Text');
+ fireEvent(subject, 'focus');
+ fireEvent.changeText(subject, 'Text');
+ fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ fireEvent(subject, 'layout', layoutEvent);
+
+ expect(onFocus).not.toHaveBeenCalled();
+ expect(onChangeText).not.toHaveBeenCalled();
+ expect(onSubmitEditing).not.toHaveBeenCalled();
+ 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', () => {
+ const onFocus = jest.fn();
+ const onChangeText = jest.fn();
+ const onSubmitEditing = jest.fn();
+ const onLayout = jest.fn();
+
+ const view = render(
+
+ );
+
+ const subject = view.getByTestId('subject');
+ fireEvent(subject, 'focus');
+ fireEvent.changeText(subject, 'Text');
+ fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ 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', () => {
+ const onFocus = jest.fn();
+ const onChangeText = jest.fn();
+ const onSubmitEditing = jest.fn();
+ const onLayout = jest.fn();
+
+ const view = render(
+
+ );
+
+ const subject = view.getByTestId('subject');
+ fireEvent(subject, 'focus');
+ fireEvent.changeText(subject, 'Text');
+ fireEvent(subject, 'submitEditing', { nativeEvent: { text: 'Text' } });
+ 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__/fireEvent.test.tsx b/src/__tests__/fireEvent.test.tsx
index 0da46994e..ce1d66326 100644
--- a/src/__tests__/fireEvent.test.tsx
+++ b/src/__tests__/fireEvent.test.tsx
@@ -216,44 +216,6 @@ test('should not fire on disabled Pressable', () => {
expect(handlePress).not.toHaveBeenCalled();
});
-test('should not fire on non-editable TextInput', () => {
- const testID = 'my-text-input';
- const onChangeTextMock = jest.fn();
- const NEW_TEXT = 'New text';
-
- const { getByTestId } = render(
-
- );
-
- fireEvent.changeText(getByTestId(testID), NEW_TEXT);
- expect(onChangeTextMock).not.toHaveBeenCalled();
-});
-
-test('should not fire on non-editable TextInput with nested Text', () => {
- const placeholder = 'Test placeholder';
- const onChangeTextMock = jest.fn();
- const NEW_TEXT = 'New text';
-
- const { getByPlaceholderText } = render(
-
-
- Test text
-
-
- );
-
- fireEvent.changeText(getByPlaceholderText(placeholder), NEW_TEXT);
- expect(onChangeTextMock).not.toHaveBeenCalled();
-});
-
test('should not fire inside View with pointerEvents="none"', () => {
const onPress = jest.fn();
const screen = render(
diff --git a/src/fireEvent.ts b/src/fireEvent.ts
index f32ef6a37..c9ddac132 100644
--- a/src/fireEvent.ts
+++ b/src/fireEvent.ts
@@ -1,22 +1,13 @@
import { ReactTestInstance } from 'react-test-renderer';
-import { TextInput } from 'react-native';
import act from './act';
import { getHostParent, isHostElement } from './helpers/component-tree';
-import { filterNodeByType } from './helpers/filterNodeByType';
import { getHostComponentNames } from './helpers/host-component-names';
type EventHandler = (...args: unknown[]) => unknown;
-function isTextInput(element: ReactTestInstance) {
- // We have to test if the element type is either the `TextInput` component
- // (for composite component) or the string "TextInput" (for host component)
- // All queries return host components but since fireEvent bubbles up
- // it would trigger the parent prop without the composite component check.
- return (
- filterNodeByType(element, TextInput) ||
- filterNodeByType(element, getHostComponentNames().textInput)
- );
-}
+const isHostTextInput = (element?: ReactTestInstance) => {
+ return element?.type === getHostComponentNames().textInput;
+};
function isTouchResponder(element: ReactTestInstance) {
if (!isHostElement(element)) {
@@ -24,7 +15,7 @@ function isTouchResponder(element: ReactTestInstance) {
}
return (
- Boolean(element.props.onStartShouldSetResponder) || isTextInput(element)
+ Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element)
);
}
@@ -57,13 +48,23 @@ function isTouchEvent(eventName: string) {
return touchEventNames.includes(eventName);
}
+// Experimentally checked which events are called on non-editable TextInput
+const textInputEventsIgnoringEditableProp = [
+ 'contentSizeChange',
+ 'layout',
+ 'scroll',
+];
+
function isEventEnabled(
element: ReactTestInstance,
eventName: string,
nearestTouchResponder?: ReactTestInstance
) {
- if (isTextInput(element)) {
- return element.props.editable !== false;
+ if (isHostTextInput(nearestTouchResponder)) {
+ return (
+ nearestTouchResponder?.props.editable !== false ||
+ textInputEventsIgnoringEditableProp.includes(eventName)
+ );
}
if (isTouchEvent(eventName) && !isPointerEventEnabled(element)) {