diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 000000000..47dc3e3d8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx
index df4fc8b49..d3e2798ef 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/__tests__/fire-event.test.tsx
@@ -36,7 +36,7 @@ test('fireEvent passes event data to handler', async () => {
const onPress = jest.fn();
await render();
await fireEvent.press(screen.getByTestId('btn'), pressEventData);
- expect(onPress).toHaveBeenCalledWith(pressEventData);
+ expect(onPress.mock.calls[0][0]).toMatchObject(pressEventData);
});
test('fireEvent passes multiple parameters to handler', async () => {
@@ -46,11 +46,11 @@ test('fireEvent passes multiple parameters to handler', async () => {
expect(handlePress).toHaveBeenCalledWith('param1', 'param2', 'param3');
});
-test('fireEvent returns handler return value', async () => {
+test('fireEvent.press returns undefined when event handler returns a value', async () => {
const handler = jest.fn().mockReturnValue('result');
await render();
const result = await fireEvent.press(screen.getByTestId('btn'));
- expect(result).toBe('result');
+ expect(result).toBe(undefined);
});
test('fireEvent bubbles event to parent handler', async () => {
@@ -115,7 +115,7 @@ describe('fireEvent.scroll', () => {
);
const scrollView = screen.getByTestId('scroll');
await fireEvent.scroll(scrollView, verticalScrollEvent);
- expect(onScroll).toHaveBeenCalledWith(verticalScrollEvent);
+ expect(onScroll.mock.calls[0][0]).toMatchObject(verticalScrollEvent);
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
});
@@ -134,7 +134,7 @@ describe('fireEvent.scroll', () => {
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 200 });
});
- test('without contentOffset does not update native state', async () => {
+ test('without contentOffset scrolls to (0, 0)', async () => {
const onScroll = jest.fn();
await render(
@@ -143,8 +143,10 @@ describe('fireEvent.scroll', () => {
);
const scrollView = screen.getByTestId('scroll');
await fireEvent.scroll(scrollView, {});
- expect(onScroll).toHaveBeenCalled();
- expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined();
+ expect(onScroll.mock.calls[0][0]).toMatchObject({
+ nativeEvent: { contentOffset: { x: 0, y: 0 } },
+ });
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 0, y: 0 });
});
test('with non-finite contentOffset values uses 0', async () => {
@@ -171,10 +173,23 @@ describe('fireEvent.scroll', () => {
);
const scrollView = screen.getByTestId('scroll');
await fireEvent.scroll(scrollView, horizontalScrollEvent);
- expect(onScroll).toHaveBeenCalledWith(horizontalScrollEvent);
+ expect(onScroll.mock.calls[0][0]).toMatchObject(horizontalScrollEvent);
expect(nativeState.contentOffsetForElement.get(scrollView)).toEqual({ x: 50, y: 0 });
});
+ test('without contentOffset via fireEvent() does not update native state', async () => {
+ const onScroll = jest.fn();
+ await render(
+
+ Content
+ ,
+ );
+ const scrollView = screen.getByTestId('scroll');
+ await fireEvent(scrollView, 'scroll', { nativeEvent: {} });
+ expect(onScroll).toHaveBeenCalled();
+ expect(nativeState.contentOffsetForElement.get(scrollView)).toBeUndefined();
+ });
+
test('with non-finite x contentOffset value uses 0', async () => {
const onScroll = jest.fn();
await render(
diff --git a/src/user-event/event-builder/__tests__/base.test.ts b/src/event-builder/__tests__/base.test.ts
similarity index 100%
rename from src/user-event/event-builder/__tests__/base.test.ts
rename to src/event-builder/__tests__/base.test.ts
diff --git a/src/event-builder/__tests__/common.test.ts b/src/event-builder/__tests__/common.test.ts
new file mode 100644
index 000000000..a70e78332
--- /dev/null
+++ b/src/event-builder/__tests__/common.test.ts
@@ -0,0 +1,57 @@
+import {
+ buildBlurEvent,
+ buildFocusEvent,
+ buildResponderGrantEvent,
+ buildResponderReleaseEvent,
+ buildTouchEvent,
+} from '../common';
+
+test('buildTouchEvent returns event with touch nativeEvent', () => {
+ const event = buildTouchEvent();
+
+ expect(event.nativeEvent).toEqual({
+ changedTouches: [],
+ identifier: 0,
+ locationX: 0,
+ locationY: 0,
+ pageX: 0,
+ pageY: 0,
+ target: 0,
+ timestamp: expect.any(Number),
+ touches: [],
+ });
+ expect(event.currentTarget).toHaveProperty('measure');
+ expect(event).toHaveProperty('preventDefault');
+});
+
+test('buildResponderGrantEvent returns touch event with dispatchConfig', () => {
+ const event = buildResponderGrantEvent();
+
+ expect(event.dispatchConfig).toEqual({
+ registrationName: 'onResponderGrant',
+ });
+ expect(event.nativeEvent).toHaveProperty('touches');
+});
+
+test('buildResponderReleaseEvent returns touch event with dispatchConfig', () => {
+ const event = buildResponderReleaseEvent();
+
+ expect(event.dispatchConfig).toEqual({
+ registrationName: 'onResponderRelease',
+ });
+ expect(event.nativeEvent).toHaveProperty('touches');
+});
+
+test('buildFocusEvent returns event with target', () => {
+ const event = buildFocusEvent();
+
+ expect(event.nativeEvent).toEqual({ target: 0 });
+ expect(event).toHaveProperty('preventDefault');
+});
+
+test('buildBlurEvent returns event with target', () => {
+ const event = buildBlurEvent();
+
+ expect(event.nativeEvent).toEqual({ target: 0 });
+ expect(event).toHaveProperty('preventDefault');
+});
diff --git a/src/event-builder/__tests__/index.test.ts b/src/event-builder/__tests__/index.test.ts
new file mode 100644
index 000000000..8497e250a
--- /dev/null
+++ b/src/event-builder/__tests__/index.test.ts
@@ -0,0 +1,16 @@
+import * as eventBuilder from '..';
+
+test('re-exports all event builders', () => {
+ expect(eventBuilder.buildTouchEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildResponderGrantEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildResponderReleaseEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildFocusEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildBlurEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildScrollEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildTextChangeEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildKeyPressEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildSubmitEditingEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildEndEditingEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildTextSelectionChangeEvent).toBeInstanceOf(Function);
+ expect(eventBuilder.buildContentSizeChangeEvent).toBeInstanceOf(Function);
+});
diff --git a/src/event-builder/__tests__/scroll.test.ts b/src/event-builder/__tests__/scroll.test.ts
new file mode 100644
index 000000000..90363934c
--- /dev/null
+++ b/src/event-builder/__tests__/scroll.test.ts
@@ -0,0 +1,34 @@
+import { buildScrollEvent } from '../scroll';
+
+test('buildScrollEvent returns default scroll event', () => {
+ const event = buildScrollEvent();
+
+ expect(event.nativeEvent).toEqual({
+ contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
+ contentOffset: { y: 0, x: 0 },
+ contentSize: { height: 0, width: 0 },
+ layoutMeasurement: { height: 0, width: 0 },
+ responderIgnoreScroll: true,
+ target: 0,
+ velocity: { y: 0, x: 0 },
+ });
+});
+
+test('buildScrollEvent uses provided offset', () => {
+ const event = buildScrollEvent({ y: 100, x: 50 });
+
+ expect(event.nativeEvent.contentOffset).toEqual({ y: 100, x: 50 });
+});
+
+test('buildScrollEvent uses provided options', () => {
+ const event = buildScrollEvent(
+ { y: 0, x: 0 },
+ {
+ contentSize: { height: 1000, width: 400 },
+ layoutMeasurement: { height: 800, width: 400 },
+ },
+ );
+
+ expect(event.nativeEvent.contentSize).toEqual({ height: 1000, width: 400 });
+ expect(event.nativeEvent.layoutMeasurement).toEqual({ height: 800, width: 400 });
+});
diff --git a/src/event-builder/__tests__/text.test.ts b/src/event-builder/__tests__/text.test.ts
new file mode 100644
index 000000000..98b5f6bdd
--- /dev/null
+++ b/src/event-builder/__tests__/text.test.ts
@@ -0,0 +1,47 @@
+import {
+ buildContentSizeChangeEvent,
+ buildEndEditingEvent,
+ buildKeyPressEvent,
+ buildSubmitEditingEvent,
+ buildTextChangeEvent,
+ buildTextSelectionChangeEvent,
+} from '../text';
+
+test('buildTextChangeEvent returns event with text', () => {
+ const event = buildTextChangeEvent('Hello');
+
+ expect(event.nativeEvent).toEqual({ text: 'Hello', target: 0, eventCount: 0 });
+});
+
+test('buildKeyPressEvent returns event with key', () => {
+ const event = buildKeyPressEvent('a');
+
+ expect(event.nativeEvent).toEqual({ key: 'a' });
+});
+
+test('buildSubmitEditingEvent returns event with text', () => {
+ const event = buildSubmitEditingEvent('Hello');
+
+ expect(event.nativeEvent).toEqual({ text: 'Hello', target: 0 });
+});
+
+test('buildEndEditingEvent returns event with text', () => {
+ const event = buildEndEditingEvent('Hello');
+
+ expect(event.nativeEvent).toEqual({ text: 'Hello', target: 0 });
+});
+
+test('buildTextSelectionChangeEvent returns event with selection', () => {
+ const event = buildTextSelectionChangeEvent({ start: 0, end: 4 });
+
+ expect(event.nativeEvent).toEqual({ selection: { start: 0, end: 4 } });
+});
+
+test('buildContentSizeChangeEvent returns event with contentSize', () => {
+ const event = buildContentSizeChangeEvent({ width: 100, height: 50 });
+
+ expect(event.nativeEvent).toEqual({
+ contentSize: { width: 100, height: 50 },
+ target: 0,
+ });
+});
diff --git a/src/user-event/event-builder/base.ts b/src/event-builder/base.ts
similarity index 100%
rename from src/user-event/event-builder/base.ts
rename to src/event-builder/base.ts
diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts
new file mode 100644
index 000000000..43b6606ae
--- /dev/null
+++ b/src/event-builder/common.ts
@@ -0,0 +1,68 @@
+import { baseSyntheticEvent } from './base';
+
+/**
+ * Experimental values:
+ * - iOS: `{"changedTouches": [[Circular]], "identifier": 1, "locationX": 253, "locationY": 30.333328247070312, "pageX": 273, "pageY": 141.3333282470703, "target": 75, "timestamp": 875928682.0450834, "touches": [[Circular]]}`
+ * - Android: `{"changedTouches": [[Circular]], "identifier": 0, "locationX": 160, "locationY": 40.3636360168457, "pageX": 180, "pageY": 140.36363220214844, "target": 53, "targetSurface": -1, "timestamp": 10290805, "touches": [[Circular]]}`
+ */
+export function buildTouchEvent() {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: {
+ changedTouches: [] as unknown[],
+ identifier: 0,
+ locationX: 0,
+ locationY: 0,
+ pageX: 0,
+ pageY: 0,
+ target: 0,
+ timestamp: Date.now(),
+ touches: [] as unknown[],
+ },
+ currentTarget: { measure: () => {} },
+ };
+}
+
+export type TouchEvent = ReturnType;
+
+export function buildResponderGrantEvent() {
+ return {
+ ...buildTouchEvent(),
+ dispatchConfig: { registrationName: 'onResponderGrant' },
+ };
+}
+
+export function buildResponderReleaseEvent() {
+ return {
+ ...buildTouchEvent(),
+ dispatchConfig: { registrationName: 'onResponderRelease' },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 0, "target": 75, "text": ""}`
+ * - Android: `{"target": 53}`
+ */
+export function buildFocusEvent() {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: {
+ target: 0,
+ },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 0, "target": 75, "text": ""}`
+ * - Android: `{"target": 53}`
+ */
+export function buildBlurEvent() {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: {
+ target: 0,
+ },
+ };
+}
diff --git a/src/event-builder/index.ts b/src/event-builder/index.ts
new file mode 100644
index 000000000..007e9d2c0
--- /dev/null
+++ b/src/event-builder/index.ts
@@ -0,0 +1,3 @@
+export * from './common';
+export * from './scroll';
+export * from './text';
diff --git a/src/user-event/event-builder/scroll-view.ts b/src/event-builder/scroll.ts
similarity index 54%
rename from src/user-event/event-builder/scroll-view.ts
rename to src/event-builder/scroll.ts
index cd2626e42..610d1d956 100644
--- a/src/user-event/event-builder/scroll-view.ts
+++ b/src/event-builder/scroll.ts
@@ -1,4 +1,4 @@
-import type { Point, Size } from '../../types';
+import type { Point, Size } from '../types';
import { baseSyntheticEvent } from './base';
/**
@@ -14,25 +14,23 @@ export type ScrollEventOptions = {
* - iOS: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 5.333333333333333}, "contentSize": {"height": 1676.6666259765625, "width": 390}, "layoutMeasurement": {"height": 753, "width": 390}, "zoomScale": 1}`
* - Android: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 31.619047164916992}, "contentSize": {"height": 1624.761962890625, "width": 411.4285583496094}, "layoutMeasurement": {"height": 785.5238037109375, "width": 411.4285583496094}, "responderIgnoreScroll": true, "target": 139, "velocity": {"x": -1.3633992671966553, "y": -1.3633992671966553}}`
*/
-export const ScrollViewEventBuilder = {
- scroll: (offset: Point = { y: 0, x: 0 }, options?: ScrollEventOptions) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: {
- contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
- contentOffset: { y: offset.y, x: offset.x },
- contentSize: {
- height: options?.contentSize?.height ?? 0,
- width: options?.contentSize?.width ?? 0,
- },
- layoutMeasurement: {
- height: options?.layoutMeasurement?.height ?? 0,
- width: options?.layoutMeasurement?.width ?? 0,
- },
- responderIgnoreScroll: true,
- target: 0,
- velocity: { y: 0, x: 0 },
+export function buildScrollEvent(offset: Point = { y: 0, x: 0 }, options?: ScrollEventOptions) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: {
+ contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
+ contentOffset: { y: offset.y, x: offset.x },
+ contentSize: {
+ height: options?.contentSize?.height ?? 0,
+ width: options?.contentSize?.width ?? 0,
},
- };
- },
-};
+ layoutMeasurement: {
+ height: options?.layoutMeasurement?.height ?? 0,
+ width: options?.layoutMeasurement?.width ?? 0,
+ },
+ responderIgnoreScroll: true,
+ target: 0,
+ velocity: { y: 0, x: 0 },
+ },
+ };
+}
diff --git a/src/event-builder/text.ts b/src/event-builder/text.ts
new file mode 100644
index 000000000..c8effcb74
--- /dev/null
+++ b/src/event-builder/text.ts
@@ -0,0 +1,74 @@
+import type { Size, TextRange } from '../types';
+import { baseSyntheticEvent } from './base';
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
+ * - Android: `{"eventCount": 6, "target": 53, "text": "Tes"}`
+ */
+export function buildTextChangeEvent(text: string) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { text, target: 0, eventCount: 0 },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 3, "key": "a", "target": 75}`
+ * - Android: `{"key": "a"}`
+ */
+export function buildKeyPressEvent(key: string) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { key },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
+ * - Android: `{"target": 53, "text": "Test"}`
+ */
+export function buildSubmitEditingEvent(text: string) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { text, target: 0 },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
+ * - Android: `{"target": 53, "text": "Test"}`
+ */
+export function buildEndEditingEvent(text: string) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { text, target: 0 },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"selection": {"end": 4, "start": 4}, "target": 75}`
+ * - Android: `{"selection": {"end": 4, "start": 4}}`
+ */
+export function buildTextSelectionChangeEvent({ start, end }: TextRange) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { selection: { start, end } },
+ };
+}
+
+/**
+ * Experimental values:
+ * - iOS: `{"contentSize": {"height": 21.666666666666668, "width": 11.666666666666666}, "target": 75}`
+ * - Android: `{"contentSize": {"height": 61.45454406738281, "width": 352.7272644042969}, "target": 53}`
+ */
+export function buildContentSizeChangeEvent({ width, height }: Size) {
+ return {
+ ...baseSyntheticEvent(),
+ nativeEvent: { contentSize: { width, height }, target: 0 },
+ };
+}
diff --git a/src/fire-event.ts b/src/fire-event.ts
index ba4914ffe..e6f733642 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -8,6 +8,7 @@ import type {
import type { Fiber, HostElement } from 'test-renderer';
import { act } from './act';
+import { buildScrollEvent, buildTouchEvent } from './event-builder';
import type { EventHandler } from './event-handler';
import { getEventHandlerFromProps } from './event-handler';
import { isElementMounted } from './helpers/component-tree';
@@ -142,14 +143,28 @@ async function fireEvent(element: HostElement, eventName: EventName, ...data: un
return returnValue;
}
-fireEvent.press = async (element: HostElement, ...data: unknown[]) =>
- await fireEvent(element, 'press', ...data);
+type EventProps = Record;
-fireEvent.changeText = async (element: HostElement, ...data: unknown[]) =>
- await fireEvent(element, 'changeText', ...data);
+fireEvent.changeText = async (element: HostElement, text: string) =>
+ await fireEvent(element, 'changeText', text);
-fireEvent.scroll = async (element: HostElement, ...data: unknown[]) =>
- await fireEvent(element, 'scroll', ...data);
+fireEvent.press = async (element: HostElement, eventProps?: EventProps) => {
+ const event = buildTouchEvent();
+ if (eventProps) {
+ mergeEventProps(event, eventProps);
+ }
+
+ await fireEvent(element, 'press', event);
+};
+
+fireEvent.scroll = async (element: HostElement, eventProps?: EventProps) => {
+ const event = buildScrollEvent();
+ if (eventProps) {
+ mergeEventProps(event, eventProps);
+ }
+
+ await fireEvent(element, 'scroll', event);
+};
export { fireEvent };
@@ -193,3 +208,25 @@ function tryGetContentOffset(event: unknown): Point | null {
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/types.ts b/src/types.ts
index 8da61033c..6e32637b8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -19,6 +19,14 @@ export interface Size {
width: number;
}
+/**
+ * Range of text in a text input.
+ */
+export interface TextRange {
+ start: number;
+ end: number;
+}
+
// TS autocomplete trick
// Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
export type StringWithAutocomplete = T | (string & {});
diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts
index 61f123f7d..1ce414e04 100644
--- a/src/user-event/clear.ts
+++ b/src/user-event/clear.ts
@@ -1,10 +1,15 @@
import type { HostElement } from 'test-renderer';
+import {
+ buildBlurEvent,
+ buildEndEditingEvent,
+ buildFocusEvent,
+ buildTextSelectionChangeEvent,
+} from '../event-builder';
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 { EventBuilder } from './event-builder';
import type { UserEventInstance } from './setup';
import { emitTypingEvents } from './type/type';
import { dispatchEvent, wait } from './utils';
@@ -22,7 +27,7 @@ export async function clear(this: UserEventInstance, element: HostElement): Prom
}
// 1. Enter element
- await dispatchEvent(element, 'focus', EventBuilder.Common.focus());
+ await dispatchEvent(element, 'focus', buildFocusEvent());
// 2. Select all
const textToClear = getTextInputValue(element);
@@ -30,11 +35,7 @@ export async function clear(this: UserEventInstance, element: HostElement): Prom
start: 0,
end: textToClear.length,
};
- await dispatchEvent(
- element,
- 'selectionChange',
- EventBuilder.TextInput.selectionChange(selectionRange),
- );
+ await dispatchEvent(element, 'selectionChange', buildTextSelectionChangeEvent(selectionRange));
// 3. Press backspace with selected text
const emptyText = '';
@@ -46,6 +47,6 @@ export async function clear(this: UserEventInstance, element: HostElement): Prom
// 4. Exit element
await wait(this.config);
- await dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(emptyText));
- await dispatchEvent(element, 'blur', EventBuilder.Common.blur());
+ await dispatchEvent(element, 'endEditing', buildEndEditingEvent(emptyText));
+ await dispatchEvent(element, 'blur', buildBlurEvent());
}
diff --git a/src/user-event/event-builder/common.ts b/src/user-event/event-builder/common.ts
deleted file mode 100644
index 173c8ec7e..000000000
--- a/src/user-event/event-builder/common.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { baseSyntheticEvent } from './base';
-
-/**
- * Experimental values:
- * - iOS: `{"changedTouches": [[Circular]], "identifier": 1, "locationX": 253, "locationY": 30.333328247070312, "pageX": 273, "pageY": 141.3333282470703, "target": 75, "timestamp": 875928682.0450834, "touches": [[Circular]]}`
- * - Android: `{"changedTouches": [[Circular]], "identifier": 0, "locationX": 160, "locationY": 40.3636360168457, "pageX": 180, "pageY": 140.36363220214844, "target": 53, "targetSurface": -1, "timestamp": 10290805, "touches": [[Circular]]}`
- */
-function touch() {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: {
- changedTouches: [],
- identifier: 0,
- locationX: 0,
- locationY: 0,
- pageX: 0,
- pageY: 0,
- target: 0,
- timestamp: Date.now(),
- touches: [],
- },
- currentTarget: { measure: () => {} },
- };
-}
-
-export const CommonEventBuilder = {
- touch,
-
- responderGrant: () => {
- return {
- ...touch(),
- dispatchConfig: { registrationName: 'onResponderGrant' },
- };
- },
-
- responderRelease: () => {
- return {
- ...touch(),
- dispatchConfig: { registrationName: 'onResponderRelease' },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 0, "target": 75, "text": ""}`
- * - Android: `{"target": 53}`
- */
- focus: () => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: {
- target: 0,
- },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 0, "target": 75, "text": ""}`
- * - Android: `{"target": 53}`
- */
- blur: () => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: {
- target: 0,
- },
- };
- },
-};
diff --git a/src/user-event/event-builder/index.ts b/src/user-event/event-builder/index.ts
deleted file mode 100644
index bee87cff4..000000000
--- a/src/user-event/event-builder/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { CommonEventBuilder } from './common';
-import { ScrollViewEventBuilder } from './scroll-view';
-import { TextInputEventBuilder } from './text-input';
-
-export const EventBuilder = {
- Common: CommonEventBuilder,
- ScrollView: ScrollViewEventBuilder,
- TextInput: TextInputEventBuilder,
-};
diff --git a/src/user-event/event-builder/text-input.ts b/src/user-event/event-builder/text-input.ts
deleted file mode 100644
index 4369d0718..000000000
--- a/src/user-event/event-builder/text-input.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import type { Size } from '../../types';
-import type { TextRange } from '../utils/text-range';
-import { baseSyntheticEvent } from './base';
-
-export const TextInputEventBuilder = {
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
- * - Android: `{"eventCount": 6, "target": 53, "text": "Tes"}`
- */
- change: (text: string) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { text, target: 0, eventCount: 0 },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 3, "key": "a", "target": 75}`
- * - Android: `{"key": "a"}`
- */
- keyPress: (key: string) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { key },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
- * - Android: `{"target": 53, "text": "Test"}`
- */
- submitEditing: (text: string) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { text, target: 0 },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"eventCount": 4, "target": 75, "text": "Test"}`
- * - Android: `{"target": 53, "text": "Test"}`
- */
- endEditing: (text: string) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { text, target: 0 },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"selection": {"end": 4, "start": 4}, "target": 75}`
- * - Android: `{"selection": {"end": 4, "start": 4}}`
- */
- selectionChange: ({ start, end }: TextRange) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { selection: { start, end } },
- };
- },
-
- /**
- * Experimental values:
- * - iOS: `{"contentSize": {"height": 21.666666666666668, "width": 11.666666666666666}, "target": 75}`
- * - Android: `{"contentSize": {"height": 61.45454406738281, "width": 352.7272644042969}, "target": 53}`
- */
- contentSizeChange: ({ width, height }: Size) => {
- return {
- ...baseSyntheticEvent(),
- nativeEvent: { contentSize: { width, height }, target: 0 },
- };
- },
-};
diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts
index f869588f5..0eef523df 100644
--- a/src/user-event/paste.ts
+++ b/src/user-event/paste.ts
@@ -1,11 +1,18 @@
import type { HostElement } from 'test-renderer';
+import {
+ buildBlurEvent,
+ buildContentSizeChangeEvent,
+ buildEndEditingEvent,
+ buildFocusEvent,
+ buildTextChangeEvent,
+ buildTextSelectionChangeEvent,
+} from '../event-builder';
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 { EventBuilder } from './event-builder';
import type { UserEventInstance } from './setup';
import { dispatchEvent, getTextContentSize, wait } from './utils';
@@ -26,43 +33,31 @@ export async function paste(
}
// 1. Enter element
- await dispatchEvent(element, 'focus', EventBuilder.Common.focus());
+ await dispatchEvent(element, 'focus', buildFocusEvent());
// 2. Select all
const textToClear = getTextInputValue(element);
const rangeToClear = { start: 0, end: textToClear.length };
- await dispatchEvent(
- element,
- 'selectionChange',
- EventBuilder.TextInput.selectionChange(rangeToClear),
- );
+ await dispatchEvent(element, 'selectionChange', buildTextSelectionChangeEvent(rangeToClear));
// 3. Paste the text
nativeState.valueForElement.set(element, text);
- await dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
+ await dispatchEvent(element, 'change', buildTextChangeEvent(text));
await dispatchEvent(element, 'changeText', text);
const rangeAfter = { start: text.length, end: text.length };
- await dispatchEvent(
- element,
- 'selectionChange',
- EventBuilder.TextInput.selectionChange(rangeAfter),
- );
+ await dispatchEvent(element, 'selectionChange', buildTextSelectionChangeEvent(rangeAfter));
// According to the docs only multiline TextInput emits contentSizeChange event
// @see: https://reactnative.dev/docs/textinput#oncontentsizechange
const isMultiline = element.props.multiline === true;
if (isMultiline) {
const contentSize = getTextContentSize(text);
- await dispatchEvent(
- element,
- 'contentSizeChange',
- EventBuilder.TextInput.contentSizeChange(contentSize),
- );
+ await dispatchEvent(element, 'contentSizeChange', buildContentSizeChangeEvent(contentSize));
}
// 4. Exit element
await wait(this.config);
- await dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(text));
- await dispatchEvent(element, 'blur', EventBuilder.Common.blur());
+ await dispatchEvent(element, 'endEditing', buildEndEditingEvent(text));
+ await dispatchEvent(element, 'blur', buildBlurEvent());
}
diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts
index 6b908f9ad..6d3b69cf1 100644
--- a/src/user-event/press/press.ts
+++ b/src/user-event/press/press.ts
@@ -1,12 +1,16 @@
import type { HostElement } from 'test-renderer';
import { act } from '../../act';
+import {
+ buildResponderGrantEvent,
+ buildResponderReleaseEvent,
+ buildTouchEvent,
+} from '../../event-builder';
import { getEventHandlerFromProps } from '../../event-handler';
import { isHostElement } from '../../helpers/component-tree';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostText, isHostTextInput } from '../../helpers/host-component-names';
import { isPointerEventEnabled } from '../../helpers/pointer-events';
-import { EventBuilder } from '../event-builder';
import type { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, wait } from '../utils';
@@ -109,23 +113,23 @@ async function emitDirectPressEvents(
options: BasePressOptions,
) {
await wait(config);
- await dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'pressIn', buildTouchEvent());
await wait(config, options.duration);
// Long press events are emitted before `pressOut`.
if (options.type === 'longPress') {
- await dispatchEvent(element, 'longPress', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'longPress', buildTouchEvent());
}
- await dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'pressOut', buildTouchEvent());
// Regular press events are emitted after `pressOut` according to the React Native docs.
// See: https://reactnative.dev/docs/pressable#onpress
// Experimentally for very short presses (< 130ms) `press` events are actually emitted before `onPressOut`, but
// we will ignore that as in reality most pressed would be above the 130ms threshold.
if (options.type === 'press') {
- await dispatchEvent(element, 'press', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'press', buildTouchEvent());
}
}
@@ -136,12 +140,12 @@ async function emitPressabilityPressEvents(
) {
await wait(config);
- await dispatchEvent(element, 'responderGrant', EventBuilder.Common.responderGrant());
+ await dispatchEvent(element, 'responderGrant', buildResponderGrantEvent());
const duration = options.duration ?? DEFAULT_MIN_PRESS_DURATION;
await wait(config, duration);
- await dispatchEvent(element, 'responderRelease', EventBuilder.Common.responderRelease());
+ await dispatchEvent(element, 'responderRelease', buildResponderReleaseEvent());
// React Native will wait for minimal delay of DEFAULT_MIN_PRESS_DURATION
// before emitting the `pressOut` event. We need to wait here, so that
diff --git a/src/user-event/scroll/__tests__/scroll-to.test.tsx b/src/user-event/scroll/__tests__/scroll-to.test.tsx
index 6b1500f5e..5575a815b 100644
--- a/src/user-event/scroll/__tests__/scroll-to.test.tsx
+++ b/src/user-event/scroll/__tests__/scroll-to.test.tsx
@@ -135,7 +135,7 @@ describe('scrollTo()', () => {
});
await user.scrollTo(screen.getByTestId('scrollView'), { y: 200 });
expect(mapEventsToShortForm(events)).toEqual([
- ['scroll', 100, undefined],
+ ['scroll', 100, 0],
['scrollBeginDrag', 100, 0],
['scroll', 125, 0],
['scroll', 150, 0],
diff --git a/src/user-event/scroll/scroll-to.ts b/src/user-event/scroll/scroll-to.ts
index 1ac320424..e561e1e4f 100644
--- a/src/user-event/scroll/scroll-to.ts
+++ b/src/user-event/scroll/scroll-to.ts
@@ -1,12 +1,12 @@
import { stringify } from 'jest-matcher-utils';
import type { HostElement } from 'test-renderer';
+import { buildScrollEvent } from '../../event-builder';
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 { EventBuilder } from '../event-builder';
import type { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, wait } from '../utils';
import { createScrollSteps, inertialInterpolator, linearInterpolator } from './utils';
@@ -88,31 +88,19 @@ async function emitDragScrollEvents(
}
await wait(config);
- await dispatchEvent(
- element,
- 'scrollBeginDrag',
- EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions),
- );
+ await dispatchEvent(element, 'scrollBeginDrag', buildScrollEvent(scrollSteps[0], scrollOptions));
// Note: experimentally, in case of drag scroll the last scroll step
// will not trigger `scroll` event.
// See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events
for (let i = 1; i < scrollSteps.length - 1; i += 1) {
await wait(config);
- await dispatchEvent(
- element,
- 'scroll',
- EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions),
- );
+ await dispatchEvent(element, 'scroll', buildScrollEvent(scrollSteps[i], scrollOptions));
}
await wait(config);
const lastStep = scrollSteps.at(-1);
- await dispatchEvent(
- element,
- 'scrollEndDrag',
- EventBuilder.ScrollView.scroll(lastStep, scrollOptions),
- );
+ await dispatchEvent(element, 'scrollEndDrag', buildScrollEvent(lastStep, scrollOptions));
}
async function emitMomentumScrollEvents(
@@ -129,7 +117,7 @@ async function emitMomentumScrollEvents(
await dispatchEvent(
element,
'momentumScrollBegin',
- EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions),
+ buildScrollEvent(scrollSteps[0], scrollOptions),
);
// Note: experimentally, in case of momentum scroll the last scroll step
@@ -137,20 +125,12 @@ async function emitMomentumScrollEvents(
// See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events
for (let i = 1; i < scrollSteps.length; i += 1) {
await wait(config);
- await dispatchEvent(
- element,
- 'scroll',
- EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions),
- );
+ await dispatchEvent(element, 'scroll', buildScrollEvent(scrollSteps[i], scrollOptions));
}
await wait(config);
const lastStep = scrollSteps.at(-1);
- await dispatchEvent(
- element,
- 'momentumScrollEnd',
- EventBuilder.ScrollView.scroll(lastStep, scrollOptions),
- );
+ await dispatchEvent(element, 'momentumScrollEnd', buildScrollEvent(lastStep, scrollOptions));
}
function ensureScrollViewDirection(element: HostElement, options: ScrollToOptions) {
diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts
index 4d1d040b3..c823ce75c 100644
--- a/src/user-event/type/type.ts
+++ b/src/user-event/type/type.ts
@@ -1,11 +1,21 @@
import type { HostElement } from 'test-renderer';
+import {
+ buildBlurEvent,
+ buildContentSizeChangeEvent,
+ buildEndEditingEvent,
+ buildFocusEvent,
+ buildKeyPressEvent,
+ buildSubmitEditingEvent,
+ buildTextChangeEvent,
+ buildTextSelectionChangeEvent,
+ buildTouchEvent,
+} from '../../event-builder';
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 { EventBuilder } from '../event-builder';
import type { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, getTextContentSize, wait } from '../utils';
import { parseKeys } from './parse-keys';
@@ -37,14 +47,14 @@ export async function type(
const keys = parseKeys(text);
if (!options?.skipPress) {
- await dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'pressIn', buildTouchEvent());
}
- await dispatchEvent(element, 'focus', EventBuilder.Common.focus());
+ await dispatchEvent(element, 'focus', buildFocusEvent());
if (!options?.skipPress) {
await wait(this.config);
- await dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
+ await dispatchEvent(element, 'pressOut', buildTouchEvent());
}
let currentText = getTextInputValue(element);
@@ -66,12 +76,12 @@ export async function type(
await wait(this.config);
if (options?.submitEditing) {
- await dispatchEvent(element, 'submitEditing', EventBuilder.TextInput.submitEditing(finalText));
+ await dispatchEvent(element, 'submitEditing', buildSubmitEditingEvent(finalText));
}
if (!options?.skipBlur) {
- await dispatchEvent(element, 'endEditing', EventBuilder.TextInput.endEditing(finalText));
- await dispatchEvent(element, 'blur', EventBuilder.Common.blur());
+ await dispatchEvent(element, 'endEditing', buildEndEditingEvent(finalText));
+ await dispatchEvent(element, 'blur', buildBlurEvent());
}
}
@@ -89,7 +99,7 @@ export async function emitTypingEvents(
const isMultiline = element.props.multiline === true;
await wait(config);
- await dispatchEvent(element, 'keyPress', EventBuilder.TextInput.keyPress(key));
+ await dispatchEvent(element, 'keyPress', buildKeyPressEvent(key));
// Platform difference (based on experiments):
// - iOS and RN Web: TextInput emits only `keyPress` event when max length has been reached
@@ -99,28 +109,20 @@ export async function emitTypingEvents(
}
nativeState.valueForElement.set(element, text);
- await dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
+ await dispatchEvent(element, 'change', buildTextChangeEvent(text));
await dispatchEvent(element, 'changeText', text);
const selectionRange = {
start: text.length,
end: text.length,
};
- await dispatchEvent(
- element,
- 'selectionChange',
- EventBuilder.TextInput.selectionChange(selectionRange),
- );
+ await dispatchEvent(element, 'selectionChange', buildTextSelectionChangeEvent(selectionRange));
// According to the docs only multiline TextInput emits contentSizeChange event
// @see: https://reactnative.dev/docs/textinput#oncontentsizechange
if (isMultiline) {
const contentSize = getTextContentSize(text);
- await dispatchEvent(
- element,
- 'contentSizeChange',
- EventBuilder.TextInput.contentSizeChange(contentSize),
- );
+ await dispatchEvent(element, 'contentSizeChange', buildContentSizeChangeEvent(contentSize));
}
}
diff --git a/src/user-event/utils/__tests__/dispatch-event.test.tsx b/src/user-event/utils/__tests__/dispatch-event.test.tsx
index 81f308e2d..cf8f5e49e 100644
--- a/src/user-event/utils/__tests__/dispatch-event.test.tsx
+++ b/src/user-event/utils/__tests__/dispatch-event.test.tsx
@@ -2,10 +2,10 @@ import * as React from 'react';
import { Text } from 'react-native';
import { render, screen } from '../../..';
-import { EventBuilder } from '../../event-builder';
+import { buildTouchEvent } from '../../../event-builder';
import { dispatchEvent } from '../dispatch-event';
-const TOUCH_EVENT = EventBuilder.Common.touch();
+const TOUCH_EVENT = buildTouchEvent();
describe('dispatchEvent', () => {
it('does dispatch event', async () => {
diff --git a/src/user-event/utils/index.ts b/src/user-event/utils/index.ts
index 9b738ad7b..28acda681 100644
--- a/src/user-event/utils/index.ts
+++ b/src/user-event/utils/index.ts
@@ -1,4 +1,3 @@
export * from './content-size';
export * from './dispatch-event';
-export * from './text-range';
export * from './wait';
diff --git a/src/user-event/utils/text-range.ts b/src/user-event/utils/text-range.ts
deleted file mode 100644
index 31a2cf593..000000000
--- a/src/user-event/utils/text-range.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface TextRange {
- start: number;
- end: number;
-}