diff --git a/contributing/event-dispatch.md b/contributing/event-dispatch.md
index f340cd549..9dfb0876c 100644
--- a/contributing/event-dispatch.md
+++ b/contributing/event-dispatch.md
@@ -2,9 +2,23 @@
RNTL has two ways to trigger events. Neither goes through React Native's native event system. Both find `on*` props in the rendered tree and call them inside `act()`.
+Both are built on the shared event subsystem in `src/events/`, which also holds `fireEvent` itself:
+
+| File | Contents |
+| ------------------------------------------- | --------------------------------------------------------------------------------------- |
+| `fire-event.ts` | Public `fireEvent` API |
+| `handler.ts` | Finding the `on*` handler for an event name in props |
+| `propagation.ts` | Bubbling vs direct events, walking up host and composite elements |
+| `is-enabled.ts` | Whether a device would deliver the event: `pointerEvents`, `editable`, touch responders |
+| `dispatch.ts` | `dispatchEvent()`: calls the target's own handler in `act()`, used by `userEvent` |
+| `builders/` | Event payloads, matching what React Native sends on a device |
+| `native-state.ts`, `update-native-state.ts` | [Native state](native-state.md) and how `fireEvent` updates it |
+
+`src/user-event/` is a separate module on top of `src/events/` and imports it only through `src/events/index.ts`.
+
## `fireEvent`
-`src/fire-event.ts` calls a single handler for a single event. The work is in finding the right handler:
+`src/events/fire-event.ts` is the public API. It calls a single handler for a single event, found with `findEventHandler()` from `src/events/propagation.ts`. The work is in finding the right handler:
- It starts at the target and moves up the tree until it finds a handler. It also checks props of composite components, not only host elements.
- Direct events (see [Native event propagation](native-events.md)) only check the target.
@@ -20,5 +34,5 @@ Each step uses `dispatchEvent()`, which only calls the target's own handler. It
- To change which handler gets a single event, change `fireEvent`. To make an interaction more realistic, change the `userEvent` action.
- Keep `dispatchEvent()` simple.
-- Put rules that both need, like `pointerEvents` or `editable`, in shared helpers in `src/helpers/`.
+- Put event rules that both need, like the `pointerEvents` and `editable` checks, in `src/events/`. They may build on general helpers from `src/helpers/` (for example `isEditableTextInput`). Code used only by `userEvent`, like delays and scroll steps, stays in `src/user-event/`.
- Event sequences should match a real device. Check on a device before changing one, and keep the code comments explaining the observed behavior.
diff --git a/contributing/native-events.md b/contributing/native-events.md
index 7165104c4..4b6f27570 100644
--- a/contributing/native-events.md
+++ b/contributing/native-events.md
@@ -2,7 +2,7 @@
In React Native, some events **bubble** up to parent elements and others are **direct**, meaning only the element that emitted them receives them. `fireEvent` should behave the same way.
-Today, `fireEvent` treats every event as bubbling except `layout`. The list of direct events lives in `isDirectEvent()` in `src/fire-event.ts`.
+Today, `fireEvent` treats every event as bubbling except `layout`. The list of direct events lives in `isDirectEvent()` in `src/events/propagation.ts`.
## Which events are which
diff --git a/contributing/native-state.md b/contributing/native-state.md
index cc7be3301..02e132846 100644
--- a/contributing/native-state.md
+++ b/contributing/native-state.md
@@ -1,6 +1,6 @@
# Native State
-On a device, some component state lives in native views, not in React. Jest has no native views, so RNTL keeps this state itself in `src/native-state.ts`.
+On a device, some component state lives in native views, not in React. Jest has no native views, so RNTL keeps this state itself in `src/events/native-state.ts`.
## What is stored
@@ -10,7 +10,7 @@ On a device, some component state lives in native views, not in React. Jest has
## Key points
-- **Writes.** `fireEvent` and `userEvent` update native state when they simulate a change that a native view would make.
+- **Writes.** `fireEvent` and `userEvent` update native state when they simulate a change that a native view would make. `fireEvent` does it through `updateNativeStateFromEvent()` in `src/events/update-native-state.ts`. Each `userEvent` action writes it directly.
- **Reads.** Helpers read native state, like `getTextInputValue()` in `src/helpers/text-input.ts`. Queries and matchers use those helpers instead of reading native state directly.
- **Props win.** A controlled prop (like `value`) always takes precedence over native state.
- **No reset.** State is stored in `WeakMap`s keyed by host instance. It disappears when the instance is unmounted, so `cleanup()` doesn't need to clear it.
diff --git a/src/events/__tests__/dispatch.test.tsx b/src/events/__tests__/dispatch.test.tsx
new file mode 100644
index 000000000..6e00b0613
--- /dev/null
+++ b/src/events/__tests__/dispatch.test.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import { Text } from 'react-native';
+
+import { render, screen } from '../..';
+import { buildTouchEvent } from '../builders/common';
+import { dispatchEvent } from '../dispatch';
+
+const TOUCH_EVENT = buildTouchEvent();
+
+test('dispatchEvent calls the target handler', async () => {
+ const onPress = jest.fn();
+ await render();
+
+ await dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT);
+ expect(onPress).toHaveBeenCalledTimes(1);
+});
+
+test('dispatchEvent does not call the parent host component handler', async () => {
+ const onPressParent = jest.fn();
+ await render(
+
+
+ ,
+ );
+
+ await dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT);
+ expect(onPressParent).not.toHaveBeenCalled();
+});
+
+test('dispatchEvent does not throw when no handler is found', async () => {
+ await render(
+
+
+ ,
+ );
+
+ await expect(
+ dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT),
+ ).resolves.not.toThrow();
+});
diff --git a/src/__tests__/fire-event.test.tsx b/src/events/__tests__/fire-event.test.tsx
similarity index 95%
rename from src/__tests__/fire-event.test.tsx
rename to src/events/__tests__/fire-event.test.tsx
index 82fbcf69e..bec40de84 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/events/__tests__/fire-event.test.tsx
@@ -13,8 +13,8 @@ import {
View,
} from 'react-native';
-import { fireEvent, render, screen } from '..';
-import { _console } from '../helpers/logger';
+import { fireEvent, render, screen } from '../..';
+import { _console } from '../../helpers/logger';
import { nativeState } from '../native-state';
const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } };
@@ -37,6 +37,20 @@ test('fireEvent accepts event name with or without "on" prefix', async () => {
expect(onPress).toHaveBeenCalledTimes(2);
});
+test('fireEvent with "on" prefixed name does not call unprefixed handler props', async () => {
+ const press = jest.fn();
+ const testOnlyPress = jest.fn();
+ // @ts-expect-error Intentionally passing such props
+ await render();
+
+ await fireEvent(screen.getByTestId('view'), 'onPress');
+ expect(press).not.toHaveBeenCalled();
+ expect(testOnlyPress).not.toHaveBeenCalled();
+
+ await fireEvent(screen.getByTestId('view'), 'press');
+ expect(press).toHaveBeenCalledTimes(1);
+});
+
test('fireEvent passes event data to handler', async () => {
const onPress = jest.fn();
await render();
@@ -194,6 +208,15 @@ describe('fireEvent.changeText', () => {
expect(nativeState.valueForInstance.get(input)).toBe('new text');
});
+ test('updates native state when fired with `on*` prefixed name', async () => {
+ const onChangeText = jest.fn();
+ await render();
+ const input = screen.getByTestId('input');
+ await fireEvent(input, 'onChangeText', 'new text');
+ expect(onChangeText).toHaveBeenCalledWith('new text');
+ expect(nativeState.valueForInstance.get(input)).toBe('new text');
+ });
+
test('does not fire on non-editable TextInput', async () => {
const onChangeText = jest.fn();
await render();
@@ -324,6 +347,18 @@ describe('fireEvent.scroll', () => {
});
});
+ test('updates native state when fired with `on*` prefixed name', async () => {
+ const onScroll = jest.fn();
+ await render();
+ const scrollView = screen.getByTestId('scroll');
+ await fireEvent(scrollView, 'onScroll', verticalScrollEvent);
+ expect(onScroll).toHaveBeenCalledWith(verticalScrollEvent);
+ expect(nativeState.contentOffsetForInstance.get(scrollView)).toEqual({
+ x: 0,
+ y: 200,
+ });
+ });
+
test.each([
['onScroll', 'scroll'],
['onScrollBeginDrag', 'scrollBeginDrag'],
diff --git a/src/__tests__/event-handler.test.tsx b/src/events/__tests__/handler.test.tsx
similarity index 77%
rename from src/__tests__/event-handler.test.tsx
rename to src/events/__tests__/handler.test.tsx
index 6c615e603..f1712545e 100644
--- a/src/__tests__/event-handler.test.tsx
+++ b/src/events/__tests__/handler.test.tsx
@@ -1,10 +1,10 @@
import * as React from 'react';
import { Text, View } from 'react-native';
-import { render, screen } from '..';
-import { getEventHandlerFromProps } from '../event-handler';
+import { render, screen } from '../..';
+import { getEventHandlerFromProps, normalizeEventName } from '../handler';
-test('getEventHandler strict mode', async () => {
+test('getEventHandlerFromProps strict mode', async () => {
const onPress = jest.fn();
const testOnlyOnPress = jest.fn();
@@ -31,7 +31,7 @@ test('getEventHandler strict mode', async () => {
expect(getEventHandlerFromProps(both.props, 'onPress')).toBe(onPress);
});
-test('getEventHandler does not treat event names starting with "on" as prefixed', async () => {
+test('getEventHandlerFromProps does not treat event names starting with "on" as prefixed', async () => {
const onOnline = jest.fn();
// @ts-expect-error Intentionally passing such props
await render();
@@ -39,7 +39,7 @@ test('getEventHandler does not treat event names starting with "on" as prefixed'
expect(getEventHandlerFromProps(screen.getByTestId('view').props, 'online')).toBe(onOnline);
});
-test('getEventHandler loose mode', async () => {
+test('getEventHandlerFromProps loose mode', async () => {
const onPress = jest.fn();
const testOnlyOnPress = jest.fn();
@@ -67,3 +67,12 @@ test('getEventHandler loose mode', async () => {
);
expect(getEventHandlerFromProps(both.props, 'onPress', { loose: true })).toBe(onPress);
});
+
+test('normalizeEventName strips the `on*` prefix', () => {
+ expect(normalizeEventName('onLayout')).toBe('layout');
+ expect(normalizeEventName('onChangeText')).toBe('changeText');
+ expect(normalizeEventName('layout')).toBe('layout');
+ expect(normalizeEventName('changeText')).toBe('changeText');
+ expect(normalizeEventName('once')).toBe('once');
+ expect(normalizeEventName('on')).toBe('on');
+});
diff --git a/src/event-builder/__tests__/base.test.ts b/src/events/builders/__tests__/base.test.ts
similarity index 100%
rename from src/event-builder/__tests__/base.test.ts
rename to src/events/builders/__tests__/base.test.ts
diff --git a/src/event-builder/__tests__/common.test.ts b/src/events/builders/__tests__/common.test.ts
similarity index 100%
rename from src/event-builder/__tests__/common.test.ts
rename to src/events/builders/__tests__/common.test.ts
diff --git a/src/event-builder/__tests__/index.test.ts b/src/events/builders/__tests__/index.test.ts
similarity index 93%
rename from src/event-builder/__tests__/index.test.ts
rename to src/events/builders/__tests__/index.test.ts
index 8497e250a..548ed471a 100644
--- a/src/event-builder/__tests__/index.test.ts
+++ b/src/events/builders/__tests__/index.test.ts
@@ -13,4 +13,5 @@ test('re-exports all event builders', () => {
expect(eventBuilder.buildEndEditingEvent).toBeInstanceOf(Function);
expect(eventBuilder.buildTextSelectionChangeEvent).toBeInstanceOf(Function);
expect(eventBuilder.buildContentSizeChangeEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.mergeEventProps).toBeInstanceOf(Function);
});
diff --git a/src/events/builders/__tests__/merge.test.ts b/src/events/builders/__tests__/merge.test.ts
new file mode 100644
index 000000000..eec683e2f
--- /dev/null
+++ b/src/events/builders/__tests__/merge.test.ts
@@ -0,0 +1,35 @@
+import { buildTouchEvent } from '../common';
+import { mergeEventProps } from '../merge';
+
+test('returns the same event when no props are passed', () => {
+ const event = buildTouchEvent();
+ const nativeEvent = { ...event.nativeEvent };
+
+ expect(mergeEventProps(event)).toBe(event);
+ expect(event.nativeEvent).toEqual(nativeEvent);
+});
+
+test('deep merges nested objects and keeps other default fields', () => {
+ const event = mergeEventProps(buildTouchEvent(), { nativeEvent: { pageX: 10, pageY: 20 } });
+
+ expect(event.nativeEvent.pageX).toBe(10);
+ expect(event.nativeEvent.pageY).toBe(20);
+ expect(event.nativeEvent.locationX).toBe(0);
+ expect(typeof event.preventDefault).toBe('function');
+});
+
+test('replaces arrays and primitive values instead of merging them', () => {
+ const event = mergeEventProps(buildTouchEvent(), {
+ nativeEvent: { touches: [{ identifier: 1 }] },
+ timeStamp: 42,
+ });
+
+ expect(event.nativeEvent.touches).toEqual([{ identifier: 1 }]);
+ expect(event.timeStamp).toBe(42);
+});
+
+test('adds props that the event does not have', () => {
+ const event = mergeEventProps(buildTouchEvent(), { custom: { value: 1 } });
+
+ expect(event).toMatchObject({ custom: { value: 1 } });
+});
diff --git a/src/event-builder/__tests__/scroll.test.ts b/src/events/builders/__tests__/scroll.test.ts
similarity index 100%
rename from src/event-builder/__tests__/scroll.test.ts
rename to src/events/builders/__tests__/scroll.test.ts
diff --git a/src/event-builder/__tests__/text.test.ts b/src/events/builders/__tests__/text.test.ts
similarity index 100%
rename from src/event-builder/__tests__/text.test.ts
rename to src/events/builders/__tests__/text.test.ts
diff --git a/src/event-builder/base.ts b/src/events/builders/base.ts
similarity index 100%
rename from src/event-builder/base.ts
rename to src/events/builders/base.ts
diff --git a/src/event-builder/common.ts b/src/events/builders/common.ts
similarity index 93%
rename from src/event-builder/common.ts
rename to src/events/builders/common.ts
index 614d9c370..5c9628f37 100644
--- a/src/event-builder/common.ts
+++ b/src/events/builders/common.ts
@@ -1,3 +1,4 @@
+import type { LayoutRectangle } from '../types';
import { baseSyntheticEvent } from './base';
/**
@@ -83,16 +84,6 @@ export function buildAccessibilityActionEvent(actionName: string) {
};
}
-/**
- * Layout rectangle of an element, as measured by the layout engine.
- */
-export interface LayoutRectangle {
- x: number;
- y: number;
- width: number;
- height: number;
-}
-
/**
* Builds a layout event, as delivered to the `onLayout` handler when an element's
* size or position is measured by the layout engine.
diff --git a/src/event-builder/index.ts b/src/events/builders/index.ts
similarity index 75%
rename from src/event-builder/index.ts
rename to src/events/builders/index.ts
index 007e9d2c0..813d38feb 100644
--- a/src/event-builder/index.ts
+++ b/src/events/builders/index.ts
@@ -1,3 +1,4 @@
export * from './common';
+export * from './merge';
export * from './scroll';
export * from './text';
diff --git a/src/events/builders/merge.ts b/src/events/builders/merge.ts
new file mode 100644
index 000000000..aa068145e
--- /dev/null
+++ b/src/events/builders/merge.ts
@@ -0,0 +1,30 @@
+import type { EventProps } from '../types';
+
+/**
+ * Deep merges custom props into a built event, so tests can override only the fields they need.
+ * Nested objects are merged, other values (including arrays) are replaced. Mutates and returns
+ * the passed event.
+ */
+export function mergeEventProps(event: T, eventProps?: EventProps): T {
+ if (eventProps) {
+ mergeInto(event as EventProps, eventProps);
+ }
+
+ return event;
+}
+
+function mergeInto(target: EventProps, source: EventProps) {
+ for (const key of Object.keys(source)) {
+ const sourceValue = source[key];
+ const targetValue = target[key];
+ if (isObject(sourceValue) && isObject(targetValue)) {
+ mergeInto(targetValue, sourceValue);
+ } else {
+ target[key] = sourceValue;
+ }
+ }
+}
+
+function isObject(value: unknown): value is EventProps {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+}
diff --git a/src/event-builder/scroll.ts b/src/events/builders/scroll.ts
similarity index 96%
rename from src/event-builder/scroll.ts
rename to src/events/builders/scroll.ts
index 610d1d956..a9a799371 100644
--- a/src/event-builder/scroll.ts
+++ b/src/events/builders/scroll.ts
@@ -1,4 +1,4 @@
-import type { Point, Size } from '../types';
+import type { Point, Size } from '../../types';
import { baseSyntheticEvent } from './base';
/**
diff --git a/src/event-builder/text.ts b/src/events/builders/text.ts
similarity index 97%
rename from src/event-builder/text.ts
rename to src/events/builders/text.ts
index 0478f9817..8b0346484 100644
--- a/src/event-builder/text.ts
+++ b/src/events/builders/text.ts
@@ -1,4 +1,4 @@
-import type { Size, TextRange } from '../types';
+import type { Size, TextRange } from '../../types';
import { baseSyntheticEvent } from './base';
/**
diff --git a/src/user-event/utils/dispatch-event.ts b/src/events/dispatch.ts
similarity index 77%
rename from src/user-event/utils/dispatch-event.ts
rename to src/events/dispatch.ts
index 314839fb2..941f90d34 100644
--- a/src/user-event/utils/dispatch-event.ts
+++ b/src/events/dispatch.ts
@@ -1,8 +1,8 @@
import type { TestInstance } from 'test-renderer';
-import { act } from '../../act';
-import { getEventHandlerFromProps } from '../../event-handler';
-import { isInstanceMounted } from '../../helpers/component-tree';
+import { act } from '../act';
+import { isInstanceMounted } from '../helpers/component-tree';
+import { getEventHandlerFromProps } from './handler';
/**
* Basic dispatch event function used by User Event module.
diff --git a/src/events/fire-event.ts b/src/events/fire-event.ts
new file mode 100644
index 000000000..890654726
--- /dev/null
+++ b/src/events/fire-event.ts
@@ -0,0 +1,55 @@
+import type { TestInstance } from 'test-renderer';
+
+import { act } from '../act';
+import { isInstanceMounted } from '../helpers/component-tree';
+import { isHostScrollView } from '../helpers/host-component-names';
+import { buildLayoutEvent, buildTouchEvent } from './builders/common';
+import { mergeEventProps } from './builders/merge';
+import { buildScrollEvent } from './builders/scroll';
+import { normalizeEventName } from './handler';
+import { nativeState } from './native-state';
+import { findEventHandler } from './propagation';
+import type { EventName, EventProps, LayoutRectangle } from './types';
+import { updateNativeStateFromEvent } from './update-native-state';
+
+async function fireEvent(instance: TestInstance, eventName: EventName, ...data: unknown[]) {
+ if (!isInstanceMounted(instance)) {
+ return;
+ }
+
+ // `fireEvent` accepts event names with and without the `on*` prefix.
+ updateNativeStateFromEvent(instance, normalizeEventName(eventName), data[0]);
+
+ const handler = findEventHandler(instance, eventName);
+ if (!handler) {
+ return;
+ }
+
+ let returnValue;
+ await act(() => {
+ returnValue = handler(...data);
+ });
+
+ return returnValue;
+}
+
+fireEvent.changeText = async (instance: TestInstance, text: string) =>
+ await fireEvent(instance, 'changeText', text);
+
+fireEvent.press = async (instance: TestInstance, eventProps?: EventProps) => {
+ await fireEvent(instance, 'press', mergeEventProps(buildTouchEvent(), eventProps));
+};
+
+fireEvent.scroll = async (instance: TestInstance, eventProps?: EventProps) => {
+ const layoutMeasurement = isHostScrollView(instance)
+ ? nativeState.layoutSizeForInstance.get(instance)
+ : undefined;
+ const event = buildScrollEvent(undefined, { layoutMeasurement });
+ await fireEvent(instance, 'scroll', mergeEventProps(event, eventProps));
+};
+
+fireEvent.layout = async (instance: TestInstance, layout?: Partial) => {
+ await fireEvent(instance, 'layout', buildLayoutEvent(layout));
+};
+
+export { fireEvent };
diff --git a/src/event-handler.ts b/src/events/handler.ts
similarity index 66%
rename from src/event-handler.ts
rename to src/events/handler.ts
index 2abcd9415..06bf2e006 100644
--- a/src/event-handler.ts
+++ b/src/events/handler.ts
@@ -1,4 +1,4 @@
-export type EventHandler = (...args: unknown[]) => unknown;
+import type { EventHandler } from './types';
export type EventHandlerOptions = {
/** Include check for event handler named without adding `on*` prefix. */
@@ -30,14 +30,31 @@ export function getEventHandlerFromProps(
return undefined;
}
+/**
+ * Returns the event name without the `on*` prefix, e.g. `onLayout` -> `layout`.
+ * Note: `fireEvent` accepts event names with and without the prefix, so use this
+ * before comparing event names.
+ */
+export function normalizeEventName(eventName: string) {
+ if (hasOnPrefix(eventName)) {
+ return eventName.charAt(2).toLowerCase() + eventName.slice(3);
+ }
+
+ return eventName;
+}
+
function getEventHandlerName(eventName: string) {
- if (/^on[A-Z]/.test(eventName)) {
+ if (hasOnPrefix(eventName)) {
return eventName;
}
return `on${capitalizeFirstLetter(eventName)}`;
}
+function hasOnPrefix(eventName: string) {
+ return /^on[A-Z]/.test(eventName);
+}
+
function capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
diff --git a/src/events/index.ts b/src/events/index.ts
new file mode 100644
index 000000000..7e5961058
--- /dev/null
+++ b/src/events/index.ts
@@ -0,0 +1,7 @@
+export * from './builders/common';
+export * from './builders/scroll';
+export * from './builders/text';
+export * from './dispatch';
+export * from './handler';
+export { isPointerEventEnabled } from './is-enabled';
+export * from './native-state';
diff --git a/src/events/is-enabled.ts b/src/events/is-enabled.ts
new file mode 100644
index 000000000..cb7d46475
--- /dev/null
+++ b/src/events/is-enabled.ts
@@ -0,0 +1,74 @@
+import { StyleSheet } from 'react-native';
+import type { TestInstance } from 'test-renderer';
+
+import { isHostTextInput } from '../helpers/host-component-names';
+import { isEditableTextInput } from '../helpers/text-input';
+
+/**
+ * pointerEvents controls whether the View can be the target of touch events.
+ * 'auto': The View and its children can be the target of touch events.
+ * 'none': The View is never the target of touch events.
+ * 'box-none': The View is never the target of touch events but its subviews can be
+ * 'box-only': The view can be the target of touch events but its subviews cannot be
+ * see the official react native doc https://reactnative.dev/docs/view#pointerevents */
+export const isPointerEventEnabled = (instance: TestInstance, isParent?: boolean): boolean => {
+ // Check both props.pointerEvents and props.style.pointerEvents
+ const pointerEvents =
+ instance?.props.pointerEvents ?? StyleSheet.flatten(instance?.props.style)?.pointerEvents;
+
+ const parentCondition = isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none';
+
+ if (pointerEvents === 'none' || parentCondition) {
+ return false;
+ }
+
+ if (!instance.parent) {
+ return true;
+ }
+
+ return isPointerEventEnabled(instance.parent, true);
+};
+
+export function isTouchResponder(instance: TestInstance) {
+ return Boolean(instance.props.onStartShouldSetResponder) || isHostTextInput(instance);
+}
+
+/**
+ * List of events affected by `pointerEvents` prop.
+ */
+const eventsAffectedByPointerEventsProp = new Set(['press']);
+
+/**
+ * List of `TextInput` events not affected by `editable` prop.
+ */
+const textInputEventsIgnoringEditableProp = new Set(['contentSizeChange', 'layout', 'scroll']);
+
+/**
+ * Checks whether a device would deliver the event to the instance, taking into account
+ * `pointerEvents`, non-editable `TextInput` and touch responders that decline the touch.
+ * Expects event name without the `on*` prefix (see `normalizeEventName`).
+ */
+export function isEventEnabled(
+ instance: TestInstance,
+ eventName: string,
+ nearestTouchResponder?: TestInstance,
+) {
+ if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) {
+ return (
+ isEditableTextInput(nearestTouchResponder) ||
+ textInputEventsIgnoringEditableProp.has(eventName)
+ );
+ }
+
+ if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(instance)) {
+ return false;
+ }
+
+ const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.();
+ const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.();
+ if (touchStart || touchMove) {
+ return true;
+ }
+
+ return touchStart === undefined && touchMove === undefined;
+}
diff --git a/src/native-state.ts b/src/events/native-state.ts
similarity index 92%
rename from src/native-state.ts
rename to src/events/native-state.ts
index 14107f042..c4b405c7c 100644
--- a/src/native-state.ts
+++ b/src/events/native-state.ts
@@ -1,6 +1,6 @@
import type { TestInstance } from 'test-renderer';
-import type { Point, Size } from './types';
+import type { Point, Size } from '../types';
/**
* Simulated native state for unmanaged controls.
diff --git a/src/events/propagation.ts b/src/events/propagation.ts
new file mode 100644
index 000000000..e607f4a9a
--- /dev/null
+++ b/src/events/propagation.ts
@@ -0,0 +1,82 @@
+import type { Fiber, TestInstance } from 'test-renderer';
+
+import { formatElement } from '../helpers/format-element';
+import { logger } from '../helpers/logger';
+import { getEventHandlerFromProps, normalizeEventName } from './handler';
+import { isEventEnabled, isTouchResponder } from './is-enabled';
+import type { EventHandler } from './types';
+
+/**
+ * Direct events are delivered by React Native only to the emitting element and do not bubble.
+ */
+export function isDirectEvent(eventName: string) {
+ return eventName === 'layout';
+}
+
+/**
+ * Finds the handler that should receive the event, as `fireEvent` does: direct events only
+ * check the target, other events bubble up the tree until an enabled handler is found.
+ *
+ * Note: handlers are looked up by the event name as passed, while event rules (direct events,
+ * `isEventEnabled`) use the name without the `on*` prefix.
+ */
+export function findEventHandler(instance: TestInstance, eventName: string): EventHandler | null {
+ return isDirectEvent(normalizeEventName(eventName))
+ ? getOwnEventHandler(instance, eventName)
+ : findBubblingEventHandler(instance, eventName);
+}
+
+function getOwnEventHandler(instance: TestInstance, eventName: string): EventHandler | null {
+ const handler = getEventHandlerFromProps(instance.props, eventName, { loose: true });
+ if (!handler) {
+ logger.warn(
+ `fireEvent: element has no handler for "${eventName}" event.`,
+ formatElement(instance),
+ );
+ return null;
+ }
+
+ return handler;
+}
+
+function findBubblingEventHandler(
+ instance: TestInstance,
+ eventName: string,
+ nearestTouchResponder?: TestInstance,
+): EventHandler | null {
+ const touchResponder = isTouchResponder(instance) ? instance : nearestTouchResponder;
+
+ const handler =
+ getEventHandlerFromProps(instance.props, eventName, { loose: true }) ??
+ findEventHandlerFromFiber(instance.unstable_fiber, eventName);
+ if (handler && isEventEnabled(instance, normalizeEventName(eventName), touchResponder)) {
+ return handler;
+ }
+
+ if (instance.parent === null) {
+ return null;
+ }
+
+ return findBubblingEventHandler(instance.parent, eventName, touchResponder);
+}
+
+function findEventHandlerFromFiber(fiber: Fiber | null, eventName: string): EventHandler | null {
+ // Container fibers have memoizedProps set to null
+ if (!fiber?.memoizedProps) {
+ return null;
+ }
+
+ const handler = getEventHandlerFromProps(fiber.memoizedProps, eventName, {
+ loose: true,
+ });
+ if (handler) {
+ return handler;
+ }
+
+ // No parent fiber or we reached another host element
+ if (fiber.return === null || typeof fiber.return.type === 'string') {
+ return null;
+ }
+
+ return findEventHandlerFromFiber(fiber.return, eventName);
+}
diff --git a/src/events/types.ts b/src/events/types.ts
new file mode 100644
index 000000000..bce54d5c6
--- /dev/null
+++ b/src/events/types.ts
@@ -0,0 +1,36 @@
+import type {
+ PressableProps,
+ ScrollViewProps,
+ TextInputProps,
+ TextProps,
+ ViewProps,
+} from 'react-native';
+
+import type { StringWithAutocomplete } from '../types';
+
+export type EventHandler = (...args: unknown[]) => unknown;
+
+export type EventProps = Record;
+
+// String union type of keys of T that start with on, stripped of 'on'
+type EventNameExtractor = keyof {
+ [K in keyof T as K extends `on${infer Rest}` ? Uncapitalize : never]: T[K];
+};
+
+export type EventName = StringWithAutocomplete<
+ | EventNameExtractor
+ | EventNameExtractor
+ | EventNameExtractor
+ | EventNameExtractor
+ | EventNameExtractor
+>;
+
+/**
+ * Layout rectangle of an element, as measured by the layout engine.
+ */
+export interface LayoutRectangle {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
diff --git a/src/events/update-native-state.ts b/src/events/update-native-state.ts
new file mode 100644
index 000000000..982287a6d
--- /dev/null
+++ b/src/events/update-native-state.ts
@@ -0,0 +1,82 @@
+import type { TestInstance } from 'test-renderer';
+
+import { isHostScrollView } from '../helpers/host-component-names';
+import { isEditableTextInput } from '../helpers/text-input';
+import type { Point, Size } from '../types';
+import { nativeState } from './native-state';
+
+const scrollEventNames = new Set([
+ 'scroll',
+ 'scrollBeginDrag',
+ 'scrollEndDrag',
+ 'momentumScrollBegin',
+ 'momentumScrollEnd',
+]);
+
+/**
+ * Updates native state the way a device would have before emitting the event.
+ * Expects event name without the `on*` prefix (see `normalizeEventName`).
+ */
+export function updateNativeStateFromEvent(
+ instance: TestInstance,
+ eventName: string,
+ value: unknown,
+) {
+ if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(instance)) {
+ nativeState.valueForInstance.set(instance, value);
+ }
+
+ if (scrollEventNames.has(eventName) && isHostScrollView(instance)) {
+ const contentOffset = tryGetContentOffset(value);
+ if (contentOffset) {
+ nativeState.contentOffsetForInstance.set(instance, contentOffset);
+ }
+ }
+
+ if (eventName === 'layout') {
+ const layoutSize = tryGetLayoutSize(value);
+ if (layoutSize) {
+ nativeState.layoutSizeForInstance.set(instance, layoutSize);
+ }
+ }
+}
+
+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;
+}
+
+function tryGetLayoutSize(event: unknown): Size | null {
+ try {
+ // @ts-expect-error: try to extract layout from the event value
+ const layout = event?.nativeEvent?.layout;
+ const width = layout?.width;
+ const height = layout?.height;
+
+ if (typeof width === 'number' || typeof height === 'number') {
+ return {
+ width: Number.isFinite(width) ? width : 0,
+ height: Number.isFinite(height) ? height : 0,
+ };
+ }
+ } catch {
+ // Do nothing
+ }
+
+ return null;
+}
diff --git a/src/fire-event.ts b/src/fire-event.ts
deleted file mode 100644
index c5a6742f6..000000000
--- a/src/fire-event.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import type {
- PressableProps,
- ScrollViewProps,
- TextInputProps,
- TextProps,
- ViewProps,
-} from 'react-native';
-import type { Fiber, TestInstance } from 'test-renderer';
-
-import { act } from './act';
-import type { LayoutRectangle } from './event-builder';
-import { buildLayoutEvent, buildScrollEvent, buildTouchEvent } from './event-builder';
-import type { EventHandler } from './event-handler';
-import { getEventHandlerFromProps } from './event-handler';
-import { isInstanceMounted } from './helpers/component-tree';
-import { formatElement } from './helpers/format-element';
-import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
-import { logger } from './helpers/logger';
-import { isPointerEventEnabled } from './helpers/pointer-events';
-import { isEditableTextInput } from './helpers/text-input';
-import { nativeState } from './native-state';
-import type { Point, Size, StringWithAutocomplete } from './types';
-
-function isTouchResponder(instance: TestInstance) {
- return Boolean(instance.props.onStartShouldSetResponder) || isHostTextInput(instance);
-}
-
-/**
- * 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',
-]);
-
-function isEventEnabled(
- instance: TestInstance,
- eventName: string,
- nearestTouchResponder?: TestInstance,
-) {
- if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) {
- return (
- isEditableTextInput(nearestTouchResponder) ||
- textInputEventsIgnoringEditableProp.has(eventName)
- );
- }
-
- if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(instance)) {
- return false;
- }
-
- const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.();
- const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.();
- if (touchStart || touchMove) {
- return true;
- }
-
- return touchStart === undefined && touchMove === undefined;
-}
-
-/**
- * Direct events are delivered by React Native only to the emitting element and do not bubble.
- * Note: `fireEvent` accepts both `layout` and `onLayout` event names, so check both forms.
- */
-function isDirectEvent(eventName: string) {
- return eventName === 'layout' || eventName === 'onLayout';
-}
-
-function getOwnEventHandler(instance: TestInstance, eventName: string): EventHandler | null {
- const handler = getEventHandlerFromProps(instance.props, eventName, { loose: true });
- if (!handler) {
- logger.warn(
- `fireEvent: element has no handler for "${eventName}" event.`,
- formatElement(instance),
- );
- return null;
- }
-
- return handler;
-}
-
-function findEventHandler(
- instance: TestInstance,
- eventName: string,
- nearestTouchResponder?: TestInstance,
-): EventHandler | null {
- const touchResponder = isTouchResponder(instance) ? instance : nearestTouchResponder;
-
- const handler =
- getEventHandlerFromProps(instance.props, eventName, { loose: true }) ??
- findEventHandlerFromFiber(instance.unstable_fiber, eventName);
- if (handler && isEventEnabled(instance, eventName, touchResponder)) {
- return handler;
- }
-
- if (instance.parent === null) {
- return null;
- }
-
- return findEventHandler(instance.parent, eventName, touchResponder);
-}
-
-function findEventHandlerFromFiber(fiber: Fiber | null, eventName: string): EventHandler | null {
- // Container fibers have memoizedProps set to null
- if (!fiber?.memoizedProps) {
- return null;
- }
-
- const handler = getEventHandlerFromProps(fiber.memoizedProps, eventName, {
- loose: true,
- });
- if (handler) {
- return handler;
- }
-
- // No parent fiber or we reached another host element
- if (fiber.return === null || typeof fiber.return.type === 'string') {
- return null;
- }
-
- return findEventHandlerFromFiber(fiber.return, eventName);
-}
-
-// String union type of keys of T that start with on, stripped of 'on'
-type EventNameExtractor = keyof {
- [K in keyof T as K extends `on${infer Rest}` ? Uncapitalize : never]: T[K];
-};
-
-type EventName = StringWithAutocomplete<
- | EventNameExtractor
- | EventNameExtractor
- | EventNameExtractor
- | EventNameExtractor
- | EventNameExtractor
->;
-
-async function fireEvent(instance: TestInstance, eventName: EventName, ...data: unknown[]) {
- if (!isInstanceMounted(instance)) {
- return;
- }
-
- setNativeStateIfNeeded(instance, eventName, data[0]);
-
- const handler = isDirectEvent(eventName)
- ? getOwnEventHandler(instance, eventName)
- : findEventHandler(instance, eventName);
- if (!handler) {
- return;
- }
-
- let returnValue;
- await act(() => {
- returnValue = handler(...data);
- });
-
- return returnValue;
-}
-
-type EventProps = Record;
-
-fireEvent.changeText = async (instance: TestInstance, text: string) =>
- await fireEvent(instance, 'changeText', text);
-
-fireEvent.press = async (instance: TestInstance, eventProps?: EventProps) => {
- const event = buildTouchEvent();
- if (eventProps) {
- mergeEventProps(event, eventProps);
- }
-
- await fireEvent(instance, 'press', event);
-};
-
-fireEvent.scroll = async (instance: TestInstance, eventProps?: EventProps) => {
- const layoutMeasurement = isHostScrollView(instance)
- ? nativeState.layoutSizeForInstance.get(instance)
- : undefined;
- const event = buildScrollEvent(undefined, { layoutMeasurement });
- if (eventProps) {
- mergeEventProps(event, eventProps);
- }
-
- await fireEvent(instance, 'scroll', event);
-};
-
-fireEvent.layout = async (instance: TestInstance, layout?: Partial) => {
- await fireEvent(instance, 'layout', buildLayoutEvent(layout));
-};
-
-export { fireEvent };
-
-const scrollEventNames = new Set([
- 'scroll',
- 'scrollBeginDrag',
- 'scrollEndDrag',
- 'momentumScrollBegin',
- 'momentumScrollEnd',
-]);
-
-function setNativeStateIfNeeded(instance: TestInstance, eventName: string, value: unknown) {
- if (eventName === 'changeText' && typeof value === 'string' && isEditableTextInput(instance)) {
- nativeState.valueForInstance.set(instance, value);
- }
-
- if (scrollEventNames.has(eventName) && isHostScrollView(instance)) {
- const contentOffset = tryGetContentOffset(value);
- if (contentOffset) {
- nativeState.contentOffsetForInstance.set(instance, contentOffset);
- }
- }
-
- if (eventName === 'layout' || eventName === 'onLayout') {
- const layoutSize = tryGetLayoutSize(value);
- if (layoutSize) {
- nativeState.layoutSizeForInstance.set(instance, layoutSize);
- }
- }
-}
-
-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;
-}
-
-function tryGetLayoutSize(event: unknown): Size | null {
- try {
- // @ts-expect-error: try to extract layout from the event value
- const layout = event?.nativeEvent?.layout;
- const width = layout?.width;
- const height = layout?.height;
-
- if (typeof width === 'number' || typeof height === 'number') {
- return {
- width: Number.isFinite(width) ? width : 0,
- height: Number.isFinite(height) ? height : 0,
- };
- }
- } catch {
- // Do nothing
- }
-
- return null;
-}
-
-function mergeEventProps(target: Record, source: Record) {
- for (const key of Object.keys(source)) {
- const sourceValue = source[key];
- const targetValue = target[key];
- if (
- sourceValue != null &&
- typeof sourceValue === 'object' &&
- !Array.isArray(sourceValue) &&
- targetValue &&
- typeof targetValue === 'object' &&
- !Array.isArray(targetValue)
- ) {
- mergeEventProps(
- targetValue as Record,
- sourceValue as Record,
- );
- } else {
- target[key] = sourceValue;
- }
- }
-}
diff --git a/src/helpers/pointer-events.ts b/src/helpers/pointer-events.ts
deleted file mode 100644
index 1f7abe6e8..000000000
--- a/src/helpers/pointer-events.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { StyleSheet } from 'react-native';
-import type { TestInstance } from 'test-renderer';
-
-/**
- * pointerEvents controls whether the View can be the target of touch events.
- * 'auto': The View and its children can be the target of touch events.
- * 'none': The View is never the target of touch events.
- * 'box-none': The View is never the target of touch events but its subviews can be
- * 'box-only': The view can be the target of touch events but its subviews cannot be
- * see the official react native doc https://reactnative.dev/docs/view#pointerevents */
-export const isPointerEventEnabled = (instance: TestInstance, isParent?: boolean): boolean => {
- // Check both props.pointerEvents and props.style.pointerEvents
- const pointerEvents =
- instance?.props.pointerEvents ?? StyleSheet.flatten(instance?.props.style)?.pointerEvents;
-
- const parentCondition = isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none';
-
- if (pointerEvents === 'none' || parentCondition) {
- return false;
- }
-
- if (!instance.parent) {
- return true;
- }
-
- return isPointerEventEnabled(instance.parent, true);
-};
diff --git a/src/helpers/text-input.ts b/src/helpers/text-input.ts
index 2d8688b13..d053e0570 100644
--- a/src/helpers/text-input.ts
+++ b/src/helpers/text-input.ts
@@ -1,6 +1,6 @@
import type { TestInstance } from 'test-renderer';
-import { nativeState } from '../native-state';
+import { nativeState } from '../events/native-state';
import { isHostTextInput } from './host-component-names';
export function isEditableTextInput(instance: TestInstance) {
diff --git a/src/pure.ts b/src/pure.ts
index f74996bc7..377c1f678 100644
--- a/src/pure.ts
+++ b/src/pure.ts
@@ -1,6 +1,6 @@
export { act } from './act';
export { cleanup } from './cleanup';
-export { fireEvent } from './fire-event';
+export { fireEvent } from './events/fire-event';
export { render } from './render';
export { waitFor } from './wait-for';
export { waitForElementToBeRemoved } from './wait-for-element-to-be-removed';
diff --git a/src/user-event/accessibility-action/accessibility-action.ts b/src/user-event/accessibility-action/accessibility-action.ts
index bfa40c6fd..c32a4f0db 100644
--- a/src/user-event/accessibility-action/accessibility-action.ts
+++ b/src/user-event/accessibility-action/accessibility-action.ts
@@ -1,13 +1,12 @@
import type { AccessibilityActionInfo } from 'react-native';
import type { TestInstance } from 'test-renderer';
-import { buildAccessibilityActionEvent } from '../../event-builder';
+import { buildAccessibilityActionEvent, dispatchEvent } from '../../events';
import { computeAriaDisabled } from '../../helpers/accessibility';
import { isTestInstance } from '../../helpers/component-tree';
import { ErrorWithStack } from '../../helpers/errors';
import type { StringWithAutocomplete } from '../../types';
import type { UserEventInstance } from '../setup';
-import { dispatchEvent } from '../utils';
/**
* Standard accessibility action names recognized by React Native (`activate`,
diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts
index cd21a7c4e..54f9173cb 100644
--- a/src/user-event/clear.ts
+++ b/src/user-event/clear.ts
@@ -5,14 +5,15 @@ import {
buildEndEditingEvent,
buildFocusEvent,
buildTextSelectionChangeEvent,
-} from '../event-builder';
+ dispatchEvent,
+ isPointerEventEnabled,
+} from '../events';
import { ErrorWithStack } from '../helpers/errors';
import { isHostTextInput } from '../helpers/host-component-names';
-import { isPointerEventEnabled } from '../helpers/pointer-events';
import { getTextInputValue, isEditableTextInput } from '../helpers/text-input';
import type { UserEventInstance } from './setup';
import { emitTypingEvents } from './type/type';
-import { dispatchEvent, wait } from './utils';
+import { wait } from './utils';
export async function clear(this: UserEventInstance, instance: TestInstance): Promise {
if (!isHostTextInput(instance)) {
diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts
index ca888c87d..fa012d016 100644
--- a/src/user-event/paste.ts
+++ b/src/user-event/paste.ts
@@ -7,14 +7,15 @@ import {
buildFocusEvent,
buildTextChangeEvent,
buildTextSelectionChangeEvent,
-} from '../event-builder';
+ dispatchEvent,
+ isPointerEventEnabled,
+ nativeState,
+} from '../events';
import { ErrorWithStack } from '../helpers/errors';
import { isHostTextInput } from '../helpers/host-component-names';
-import { isPointerEventEnabled } from '../helpers/pointer-events';
import { getTextInputValue, isEditableTextInput } from '../helpers/text-input';
-import { nativeState } from '../native-state';
import type { UserEventInstance } from './setup';
-import { dispatchEvent, getTextContentSize, wait } from './utils';
+import { getTextContentSize, wait } from './utils';
export async function paste(
this: UserEventInstance,
diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts
index 6408c3174..3da5a7feb 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -5,14 +5,15 @@ import {
buildResponderGrantEvent,
buildResponderReleaseEvent,
buildTouchEvent,
-} from '../../event-builder';
-import { getEventHandlerFromProps } from '../../event-handler';
+ dispatchEvent,
+ getEventHandlerFromProps,
+ isPointerEventEnabled,
+} from '../../events';
import { isTestInstance } from '../../helpers/component-tree';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostText, isHostTextInput } from '../../helpers/host-component-names';
-import { isPointerEventEnabled } from '../../helpers/pointer-events';
import type { UserEventConfig, UserEventInstance } from '../setup';
-import { dispatchEvent, wait } from '../utils';
+import { wait } from '../utils';
// These are constants defined in the React Native repo
// See: https://github.com/facebook/react-native/blob/50e38cc9f1e6713228a91ad50f426c4f65e65e1a/packages/react-native/Libraries/Pressability/Pressability.js#L264
diff --git a/src/user-event/scroll/scroll-to.ts b/src/user-event/scroll/scroll-to.ts
index f51bf6ffb..d029ffc48 100644
--- a/src/user-event/scroll/scroll-to.ts
+++ b/src/user-event/scroll/scroll-to.ts
@@ -1,14 +1,13 @@
import { stringify } from 'jest-matcher-utils';
import type { TestInstance } from 'test-renderer';
-import { buildScrollEvent } from '../../event-builder';
+import { buildScrollEvent, dispatchEvent, nativeState } from '../../events';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostScrollView } from '../../helpers/host-component-names';
import { pick } from '../../helpers/object';
-import { nativeState } from '../../native-state';
import type { Point, Size } from '../../types';
import type { UserEventConfig, UserEventInstance } from '../setup';
-import { dispatchEvent, wait } from '../utils';
+import { wait } from '../utils';
import { createScrollSteps, inertialInterpolator, linearInterpolator } from './utils';
interface CommonScrollToOptions {
diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts
index 1672c9bf8..94c04e8ed 100644
--- a/src/user-event/type/type.ts
+++ b/src/user-event/type/type.ts
@@ -10,14 +10,15 @@ import {
buildTextChangeEvent,
buildTextSelectionChangeEvent,
buildTouchEvent,
-} from '../../event-builder';
+ dispatchEvent,
+ isPointerEventEnabled,
+ nativeState,
+} from '../../events';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostTextInput } from '../../helpers/host-component-names';
-import { isPointerEventEnabled } from '../../helpers/pointer-events';
import { getTextInputValue, isEditableTextInput } from '../../helpers/text-input';
-import { nativeState } from '../../native-state';
import type { UserEventConfig, UserEventInstance } from '../setup';
-import { dispatchEvent, getTextContentSize, wait } from '../utils';
+import { getTextContentSize, wait } from '../utils';
import { parseKeys } from './parse-keys';
export interface TypeOptions {
diff --git a/src/user-event/utils/__tests__/dispatch-event.test.tsx b/src/user-event/utils/__tests__/dispatch-event.test.tsx
deleted file mode 100644
index cf8f5e49e..000000000
--- a/src/user-event/utils/__tests__/dispatch-event.test.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import * as React from 'react';
-import { Text } from 'react-native';
-
-import { render, screen } from '../../..';
-import { buildTouchEvent } from '../../../event-builder';
-import { dispatchEvent } from '../dispatch-event';
-
-const TOUCH_EVENT = buildTouchEvent();
-
-describe('dispatchEvent', () => {
- it('does dispatch event', async () => {
- const onPress = jest.fn();
- await render();
-
- await dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT);
- expect(onPress).toHaveBeenCalledTimes(1);
- });
-
- it('does not dispatch event to parent host component', async () => {
- const onPressParent = jest.fn();
- await render(
-
-
- ,
- );
-
- await dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT);
- expect(onPressParent).not.toHaveBeenCalled();
- });
-
- it('does NOT throw if no handler found', async () => {
- await render(
-
-
- ,
- );
-
- await expect(
- dispatchEvent(screen.getByTestId('text'), 'press', TOUCH_EVENT),
- ).resolves.not.toThrow();
- });
-});
diff --git a/src/user-event/utils/index.ts b/src/user-event/utils/index.ts
index 28acda681..aea309381 100644
--- a/src/user-event/utils/index.ts
+++ b/src/user-event/utils/index.ts
@@ -1,3 +1,2 @@
export * from './content-size';
-export * from './dispatch-event';
export * from './wait';