Skip to content

refactor: fireEvent cleanup #1401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 3, 2023
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;