refactor: rewrite fireEvent tests (#1861)

This commit is contained in:
Maciej Jastrzebski
2026-01-11 10:55:35 +01:00
committed by GitHub
parent ae8f3d862b
commit 764472f742
5 changed files with 557 additions and 791 deletions
-156
View File
@@ -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 <TextInput {...props} />;
}
function DoubleWrappedTextInput(props: TextInputProps) {
return <WrappedTextInput {...props} />;
}
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(
<TextInput
editable={false}
testID="subject"
onFocus={onFocus}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onLayout={onLayout}
/>,
);
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(
<TextInput
editable={false}
testID="subject"
onFocus={onFocus}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onLayout={onLayout}
>
<Text>Nested Text</Text>
</TextInput>,
);
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(
<WrappedTextInput
editable={false}
testID="subject"
onFocus={onFocus}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onLayout={onLayout}
/>,
);
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(
<DoubleWrappedTextInput
editable={false}
testID="subject"
onFocus={onFocus}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onLayout={onLayout}
/>,
);
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);
});
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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 = () => (
<View testID="container">
+2 -5
View File
@@ -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,
-8
View File
@@ -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);
}