Files
callstack__react-native-tes…/src/fire-event.ts
T
2025-07-30 15:00:57 +02:00

179 lines
5.0 KiB
TypeScript

import type {
PressableProps,
ScrollViewProps,
TextInputProps,
TextProps,
ViewProps,
} from 'react-native';
import type { ReactTestInstance } from 'react-test-renderer';
import act from './act';
import { getEventHandler } from './event-handler';
import { isElementMounted, isHostElement } from './helpers/component-tree';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isEditableTextInput } from './helpers/text-input';
import { nativeState } from './native-state';
import type { Point, StringWithAutocomplete } from './types';
type EventHandler = (...args: unknown[]) => unknown;
export function isTouchResponder(element: ReactTestInstance) {
if (!isHostElement(element)) {
return false;
}
return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element);
}
/**
* List of events affected by `pointerEvents` prop.
*
* Note: `fireEvent` is accepting both `press` and `onPress` for event names,
* so we need cover both forms.
*/
const eventsAffectedByPointerEventsProp = new Set(['press', 'onPress']);
/**
* List of `TextInput` events not affected by `editable` prop.
*
* Note: `fireEvent` is accepting both `press` and `onPress` for event names,
* so we need cover both forms.
*/
const textInputEventsIgnoringEditableProp = new Set([
'contentSizeChange',
'onContentSizeChange',
'layout',
'onLayout',
'scroll',
'onScroll',
]);
export function isEventEnabled(
element: ReactTestInstance,
eventName: string,
nearestTouchResponder?: ReactTestInstance,
) {
if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) {
return (
isEditableTextInput(nearestTouchResponder) ||
textInputEventsIgnoringEditableProp.has(eventName)
);
}
if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(element)) {
return false;
}
const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.();
const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.();
if (touchStart || touchMove) {
return true;
}
return touchStart === undefined && touchMove === undefined;
}
function findEventHandler(
element: ReactTestInstance,
eventName: string,
nearestTouchResponder?: ReactTestInstance,
): EventHandler | null {
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
const handler = getEventHandler(element, eventName, { loose: true });
if (handler && isEventEnabled(element, eventName, touchResponder)) {
return handler;
}
if (element.parent === null) {
return null;
}
return findEventHandler(element.parent, eventName, touchResponder);
}
// String union type of keys of T that start with on, stripped of 'on'
type EventNameExtractor<T> = keyof {
[K in keyof T as K extends `on${infer Rest}` ? Uncapitalize<Rest> : never]: T[K];
};
type EventName = StringWithAutocomplete<
| EventNameExtractor<ViewProps>
| EventNameExtractor<TextProps>
| EventNameExtractor<TextInputProps>
| EventNameExtractor<PressableProps>
| EventNameExtractor<ScrollViewProps>
>;
function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: unknown[]) {
if (!isElementMounted(element)) {
return;
}
setNativeStateIfNeeded(element, eventName, data[0]);
const handler = findEventHandler(element, eventName);
if (!handler) {
return;
}
let returnValue;
void act(() => {
returnValue = handler(...data);
});
return returnValue;
}
fireEvent.press = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'press', ...data);
fireEvent.changeText = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'changeText', ...data);
fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'scroll', ...data);
export default fireEvent;
const scrollEventNames = new Set([
'scroll',
'scrollBeginDrag',
'scrollEndDrag',
'momentumScrollBegin',
'momentumScrollEnd',
]);
function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) {
if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(element)) {
nativeState.valueForElement.set(element, value);
}
if (scrollEventNames.has(eventName) && isHostScrollView(element)) {
const contentOffset = tryGetContentOffset(value);
if (contentOffset) {
nativeState.contentOffsetForElement.set(element, contentOffset);
}
}
}
function tryGetContentOffset(event: unknown): Point | null {
try {
// @ts-expect-error: try to extract contentOffset from the event value
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,
y: Number.isFinite(y) ? y : 0,
};
}
} catch {
// Do nothing
}
return null;
}