Skip to content
9 changes: 7 additions & 2 deletions src/__tests__/fireEvent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ test('should not fire inside View with pointerEvents="none"', () => {
);

fireEvent.press(screen.getByText('Trigger'));
fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});

Expand All @@ -279,6 +280,7 @@ test('should not fire inside View with pointerEvents="box-only"', () => {
);

fireEvent.press(screen.getByText('Trigger'));
fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});

Expand All @@ -293,7 +295,8 @@ test('should fire inside View with pointerEvents="box-none"', () => {
);

fireEvent.press(screen.getByText('Trigger'));
expect(onPress).toHaveBeenCalled();
fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).toHaveBeenCalledTimes(2);
});

test('should fire inside View with pointerEvents="auto"', () => {
Expand All @@ -307,7 +310,8 @@ test('should fire inside View with pointerEvents="auto"', () => {
);

fireEvent.press(screen.getByText('Trigger'));
expect(onPress).toHaveBeenCalled();
fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).toHaveBeenCalledTimes(2);
});

test('should not fire deeply inside View with pointerEvents="box-only"', () => {
Expand All @@ -323,6 +327,7 @@ test('should not fire deeply inside View with pointerEvents="box-only"', () => {
);

fireEvent.press(screen.getByText('Trigger'));
fireEvent(screen.getByText('Trigger'), 'onPress');
expect(onPress).not.toHaveBeenCalled();
});

Expand Down
126 changes: 60 additions & 66 deletions src/fireEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import { getHostParent, isHostElement } from './helpers/component-tree';
import { filterNodeByType } from './helpers/filterNodeByType';
import { getHostComponentNames } from './helpers/host-component-names';

type EventHandler = (...args: any) => unknown;

const isTextInput = (element?: ReactTestInstance) => {
if (!element) {
return false;
}
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
Expand All @@ -20,18 +16,22 @@ const isTextInput = (element?: ReactTestInstance) => {
filterNodeByType(element, TextInput) ||
filterNodeByType(element, getHostComponentNames().textInput)
);
};
}

const isTouchResponder = (element?: ReactTestInstance) => {
if (!isHostElement(element)) return false;
function isTouchResponder(element: ReactTestInstance) {
if (!isHostElement(element)) {
return false;
}

return !!element?.props.onStartShouldSetResponder || isTextInput(element);
};
return (
Boolean(element.props.onStartShouldSetResponder) || isTextInput(element)
);
}

const isPointerEventEnabled = (
function isPointerEventEnabled(
element: ReactTestInstance,
isParent?: boolean
): boolean => {
): boolean {
const pointerEvents = element.props.pointerEvents;
if (pointerEvents === 'none') {
return false;
Expand All @@ -47,54 +47,60 @@ const isPointerEventEnabled = (
}

return isPointerEventEnabled(parent, true);
};
}

// Due to accepting both `press` and `onPress` for event names, we need to
// cover both forms.
const touchEventNames = ['press', 'onPress'];

const isTouchEvent = (eventName?: string) => {
return eventName === 'press';
};
function isTouchEvent(eventName: string) {
return touchEventNames.includes(eventName);
}

const isEventEnabled = (
function isEventEnabled(
element: ReactTestInstance,
touchResponder?: ReactTestInstance,
eventName?: string
) => {
if (isTextInput(element)) return element?.props.editable !== false;
if (!isPointerEventEnabled(element) && isTouchEvent(eventName)) return false;
eventName: string,
nearestTouchResponder?: ReactTestInstance
) {
if (isTextInput(element)) {
return element.props.editable !== false;
}

const touchStart = touchResponder?.props.onStartShouldSetResponder?.();
const touchMove = touchResponder?.props.onMoveShouldSetResponder?.();
if (isTouchEvent(eventName) && !isPointerEventEnabled(element)) {
return false;
}

if (touchStart || touchMove) return true;
const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.();
const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.();
if (touchStart || touchMove) {
return true;
}

return touchStart === undefined && touchMove === undefined;
};
}

const findEventHandler = (
function findEventHandler(
element: ReactTestInstance,
eventName: string,
callsite?: any,
nearestTouchResponder?: ReactTestInstance
): EventHandler | null => {
): EventHandler | null {
const touchResponder = isTouchResponder(element)
? element
: nearestTouchResponder;

const handler = getEventHandler(element, eventName);
if (handler && isEventEnabled(element, touchResponder, eventName))
if (handler && isEventEnabled(element, eventName, touchResponder))
return handler;

if (element.parent === null || element.parent.parent === null) {
return null;
}

return findEventHandler(element.parent, eventName, callsite, touchResponder);
};
return findEventHandler(element.parent, eventName, touchResponder);
}

const getEventHandler = (
element: ReactTestInstance,
eventName: string
): EventHandler | undefined => {
const eventHandlerName = toEventHandlerName(eventName);
function getEventHandler(element: ReactTestInstance, eventName: string) {
const eventHandlerName = getEventHandlerName(eventName);
if (typeof element.props[eventHandlerName] === 'function') {
return element.props[eventHandlerName];
}
Expand All @@ -104,49 +110,37 @@ const getEventHandler = (
}

return undefined;
};
}

function getEventHandlerName(eventName: string) {
return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
}

const invokeEvent = (
function fireEvent(
element: ReactTestInstance,
eventName: string,
callsite?: any,
...data: Array<any>
) => {
const handler = findEventHandler(element, eventName, callsite);

...data: unknown[]
) {
const handler = findEventHandler(element, eventName);
if (!handler) {
return;
}

let returnValue;

act(() => {
returnValue = handler(...data);
});

return returnValue;
};

const toEventHandlerName = (eventName: string) =>
`on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
}

const pressHandler = (element: ReactTestInstance, ...data: Array<any>): void =>
invokeEvent(element, 'press', pressHandler, ...data);
const changeTextHandler = (
element: ReactTestInstance,
...data: Array<any>
): void => invokeEvent(element, 'changeText', changeTextHandler, ...data);
const scrollHandler = (element: ReactTestInstance, ...data: Array<any>): void =>
invokeEvent(element, 'scroll', scrollHandler, ...data);
fireEvent.press = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'press', ...data);

const fireEvent = (
element: ReactTestInstance,
eventName: string,
...data: Array<any>
): void => invokeEvent(element, eventName, fireEvent, ...data);
fireEvent.changeText = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'changeText', ...data);

fireEvent.press = pressHandler;
fireEvent.changeText = changeTextHandler;
fireEvent.scroll = scrollHandler;
fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) =>
fireEvent(element, 'scroll', ...data);

export default fireEvent;