From 3a47409a18c25160b541bfc618f564b96a3f45e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 13 Feb 2026 00:02:49 +0000 Subject: [PATCH 01/10] wip --- src/__tests__/fire-event.test.tsx | 10 +++---- src/fire-event.ts | 45 ++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index df4fc8b49..61471f0c8 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 }); }); @@ -171,7 +171,7 @@ 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 }); }); diff --git a/src/fire-event.ts b/src/fire-event.ts index ba4914ffe..df580ec95 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -16,6 +16,7 @@ import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; +import { EventBuilder } from './user-event/event-builder'; function isTouchResponder(element: HostElement) { return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element); @@ -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.scroll = async (element: HostElement, ...data: unknown[]) => - await fireEvent(element, 'scroll', ...data); +fireEvent.press = async (element: HostElement, eventProps?: EventProps) => { + const event = EventBuilder.Common.touch(); + if (eventProps) { + mergeEventProps(event, eventProps); + } + + await fireEvent(element, 'press', event); +}; + +fireEvent.scroll = async (element: HostElement, eventProps?: EventProps) => { + const event = EventBuilder.ScrollView.scroll(); + 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 && + typeof sourceValue === 'object' && + !Array.isArray(sourceValue) && + targetValue && + typeof targetValue === 'object' && + !Array.isArray(targetValue) + ) { + mergeEventProps( + targetValue as Record, + sourceValue as Record, + ); + } else { + target[key] = sourceValue; + } + } +} From 603505eed95bc26c0e6e7de87b1b0830fed35eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 15 Feb 2026 23:54:26 +0000 Subject: [PATCH 02/10] fix tests --- src/__tests__/fire-event.test.tsx | 8 +++++--- src/user-event/scroll/__tests__/scroll-to.test.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 61471f0c8..ce4f6dd33 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -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 () => { 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], From 931878808380b532dbab873ad07fea115e974b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:07:54 +0000 Subject: [PATCH 03/10] promote event-builder --- .../event-builder/__tests__/base.test.ts | 0 src/{user-event => }/event-builder/base.ts | 0 src/{user-event => }/event-builder/common.ts | 6 +++-- src/{user-event => }/event-builder/index.ts | 0 .../event-builder/scroll-view.ts | 2 +- .../event-builder/text-input.ts | 3 +-- src/event-builder/types.ts | 23 +++++++++++++++++++ src/fire-event.ts | 9 ++++---- src/native-state.ts | 2 +- src/types.ts | 16 ------------- src/user-event/clear.ts | 2 +- src/user-event/paste.ts | 2 +- src/user-event/press/press.ts | 2 +- src/user-event/scroll/scroll-to.ts | 4 ++-- src/user-event/scroll/utils.ts | 2 +- src/user-event/type/type.ts | 2 +- .../utils/__tests__/dispatch-event.test.tsx | 2 +- src/user-event/utils/content-size.ts | 2 +- src/user-event/utils/index.ts | 1 - src/user-event/utils/text-range.ts | 4 ---- 20 files changed, 44 insertions(+), 40 deletions(-) rename src/{user-event => }/event-builder/__tests__/base.test.ts (100%) rename src/{user-event => }/event-builder/base.ts (100%) rename src/{user-event => }/event-builder/common.ts (93%) rename src/{user-event => }/event-builder/index.ts (100%) rename src/{user-event => }/event-builder/scroll-view.ts (97%) rename src/{user-event => }/event-builder/text-input.ts (95%) create mode 100644 src/event-builder/types.ts delete mode 100644 src/user-event/utils/text-range.ts 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/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/user-event/event-builder/common.ts b/src/event-builder/common.ts similarity index 93% rename from src/user-event/event-builder/common.ts rename to src/event-builder/common.ts index 173c8ec7e..1e1499f57 100644 --- a/src/user-event/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -9,7 +9,7 @@ function touch() { return { ...baseSyntheticEvent(), nativeEvent: { - changedTouches: [], + changedTouches: [] as number[], identifier: 0, locationX: 0, locationY: 0, @@ -17,12 +17,14 @@ function touch() { pageY: 0, target: 0, timestamp: Date.now(), - touches: [], + touches: [] as number[], }, currentTarget: { measure: () => {} }, }; } +export type TouchEvent = ReturnType; + export const CommonEventBuilder = { touch, diff --git a/src/user-event/event-builder/index.ts b/src/event-builder/index.ts similarity index 100% rename from src/user-event/event-builder/index.ts rename to src/event-builder/index.ts diff --git a/src/user-event/event-builder/scroll-view.ts b/src/event-builder/scroll-view.ts similarity index 97% rename from src/user-event/event-builder/scroll-view.ts rename to src/event-builder/scroll-view.ts index cd2626e42..94334c2da 100644 --- a/src/user-event/event-builder/scroll-view.ts +++ b/src/event-builder/scroll-view.ts @@ -1,5 +1,5 @@ -import type { Point, Size } from '../../types'; import { baseSyntheticEvent } from './base'; +import type { Point, Size } from './types'; /** * Other options for constructing a scroll event. diff --git a/src/user-event/event-builder/text-input.ts b/src/event-builder/text-input.ts similarity index 95% rename from src/user-event/event-builder/text-input.ts rename to src/event-builder/text-input.ts index 4369d0718..f248219d2 100644 --- a/src/user-event/event-builder/text-input.ts +++ b/src/event-builder/text-input.ts @@ -1,6 +1,5 @@ -import type { Size } from '../../types'; -import type { TextRange } from '../utils/text-range'; import { baseSyntheticEvent } from './base'; +import type { Size, TextRange } from './types'; export const TextInputEventBuilder = { /** diff --git a/src/event-builder/types.ts b/src/event-builder/types.ts new file mode 100644 index 000000000..ca7d30480 --- /dev/null +++ b/src/event-builder/types.ts @@ -0,0 +1,23 @@ +/** + * Location of an element. + */ +export interface Point { + y: number; + x: number; +} + +/** + * Size of an element. + */ +export interface Size { + height: number; + width: number; +} + +/** + * Range of text in a text input. + */ +export interface TextRange { + start: number; + end: number; +} diff --git a/src/fire-event.ts b/src/fire-event.ts index df580ec95..54a93f017 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -8,6 +8,8 @@ import type { import type { Fiber, HostElement } from 'test-renderer'; import { act } from './act'; +import { EventBuilder } from './event-builder'; +import type { Point } from './event-builder/types'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; import { isElementMounted } from './helpers/component-tree'; @@ -15,8 +17,7 @@ import { isHostScrollView, isHostTextInput } from './helpers/host-component-name import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; -import type { Point, StringWithAutocomplete } from './types'; -import { EventBuilder } from './user-event/event-builder'; +import type { StringWithAutocomplete } from './types'; function isTouchResponder(element: HostElement) { return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element); @@ -145,8 +146,8 @@ async function fireEvent(element: HostElement, eventName: EventName, ...data: un 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.press = async (element: HostElement, eventProps?: EventProps) => { const event = EventBuilder.Common.touch(); diff --git a/src/native-state.ts b/src/native-state.ts index 6a99b86d5..8ed780905 100644 --- a/src/native-state.ts +++ b/src/native-state.ts @@ -1,6 +1,6 @@ import type { HostElement } from 'test-renderer'; -import type { Point } from './types'; +import type { Point } from './event-builder/types'; /** * Simulated native state for unmanaged controls. diff --git a/src/types.ts b/src/types.ts index 8da61033c..4b6beaa26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,22 +3,6 @@ export type RefObject = { current: T; }; -/** - * Location of an element. - */ -export interface Point { - y: number; - x: number; -} - -/** - * Size of an element. - */ -export interface Size { - height: number; - width: 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..8a0d81841 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -1,10 +1,10 @@ import type { HostElement } from 'test-renderer'; +import { EventBuilder } 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'; diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts index f869588f5..ad162255e 100644 --- a/src/user-event/paste.ts +++ b/src/user-event/paste.ts @@ -1,11 +1,11 @@ import type { HostElement } from 'test-renderer'; +import { EventBuilder } 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'; diff --git a/src/user-event/press/press.ts b/src/user-event/press/press.ts index 6b908f9ad..99f1077f4 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,12 +1,12 @@ import type { HostElement } from 'test-renderer'; import { act } from '../../act'; +import { EventBuilder } 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'; diff --git a/src/user-event/scroll/scroll-to.ts b/src/user-event/scroll/scroll-to.ts index 1ac320424..e68dc6277 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 { EventBuilder } from '../../event-builder'; +import type { Point, Size } from '../../event-builder/types'; 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'; diff --git a/src/user-event/scroll/utils.ts b/src/user-event/scroll/utils.ts index 8fd043edd..79afb3e51 100644 --- a/src/user-event/scroll/utils.ts +++ b/src/user-event/scroll/utils.ts @@ -1,4 +1,4 @@ -import type { Point } from '../../types'; +import type { Point } from '../../event-builder/types'; const DEFAULT_STEPS_COUNT = 5; diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts index 4d1d040b3..3c92d18a6 100644 --- a/src/user-event/type/type.ts +++ b/src/user-event/type/type.ts @@ -1,11 +1,11 @@ import type { HostElement } from 'test-renderer'; +import { EventBuilder } 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'; diff --git a/src/user-event/utils/__tests__/dispatch-event.test.tsx b/src/user-event/utils/__tests__/dispatch-event.test.tsx index 81f308e2d..8e3232b42 100644 --- a/src/user-event/utils/__tests__/dispatch-event.test.tsx +++ b/src/user-event/utils/__tests__/dispatch-event.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Text } from 'react-native'; import { render, screen } from '../../..'; -import { EventBuilder } from '../../event-builder'; +import { EventBuilder } from '../../../event-builder'; import { dispatchEvent } from '../dispatch-event'; const TOUCH_EVENT = EventBuilder.Common.touch(); diff --git a/src/user-event/utils/content-size.ts b/src/user-event/utils/content-size.ts index 7d83a5673..75354edaf 100644 --- a/src/user-event/utils/content-size.ts +++ b/src/user-event/utils/content-size.ts @@ -1,4 +1,4 @@ -import type { Size } from '../../types'; +import type { Size } from '../../event-builder/types'; /** * Simple function for getting mock the size of given text. 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; -} From 04b42b47be09a9a1cc3efdaf841b8461df3e69fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:16:35 +0000 Subject: [PATCH 04/10] flatten event builder --- src/event-builder/common.ts | 84 +++++++++---------- src/event-builder/index.ts | 12 +-- .../{scroll-view.ts => scroll.ts} | 40 +++++---- src/event-builder/text-input.ts | 76 ----------------- src/event-builder/text.ts | 74 ++++++++++++++++ src/fire-event.ts | 6 +- src/user-event/clear.ts | 19 +++-- src/user-event/paste.ts | 35 ++++---- src/user-event/press/press.ts | 18 ++-- src/user-event/scroll/scroll-to.ts | 34 ++------ src/user-event/type/type.ts | 40 ++++----- .../utils/__tests__/dispatch-event.test.tsx | 4 +- 12 files changed, 205 insertions(+), 237 deletions(-) rename src/event-builder/{scroll-view.ts => scroll.ts} (57%) delete mode 100644 src/event-builder/text-input.ts create mode 100644 src/event-builder/text.ts diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts index 1e1499f57..4d030ab7b 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -5,7 +5,7 @@ import { baseSyntheticEvent } from './base'; * - 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() { +export function buildTouchEvent() { return { ...baseSyntheticEvent(), nativeEvent: { @@ -23,50 +23,46 @@ function touch() { }; } -export type TouchEvent = ReturnType; +export type TouchEvent = ReturnType; -export const CommonEventBuilder = { - touch, - - responderGrant: () => { - return { - ...touch(), - dispatchConfig: { registrationName: 'onResponderGrant' }, - }; - }, +export function buildResponderGrantEvent() { + return { + ...buildTouchEvent(), + dispatchConfig: { registrationName: 'onResponderGrant' }, + }; +} - responderRelease: () => { - return { - ...touch(), - dispatchConfig: { registrationName: 'onResponderRelease' }, - }; - }, +export function buildResponderReleaseEvent() { + return { + ...buildTouchEvent(), + 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}` + */ +export function buildFocusEvent() { + return { + ...baseSyntheticEvent(), + nativeEvent: { + target: 0, + }, + }; +} - /** - * Experimental values: - * - iOS: `{"eventCount": 0, "target": 75, "text": ""}` - * - Android: `{"target": 53}` - */ - blur: () => { - 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 index bee87cff4..007e9d2c0 100644 --- a/src/event-builder/index.ts +++ b/src/event-builder/index.ts @@ -1,9 +1,3 @@ -import { CommonEventBuilder } from './common'; -import { ScrollViewEventBuilder } from './scroll-view'; -import { TextInputEventBuilder } from './text-input'; - -export const EventBuilder = { - Common: CommonEventBuilder, - ScrollView: ScrollViewEventBuilder, - TextInput: TextInputEventBuilder, -}; +export * from './common'; +export * from './scroll'; +export * from './text'; diff --git a/src/event-builder/scroll-view.ts b/src/event-builder/scroll.ts similarity index 57% rename from src/event-builder/scroll-view.ts rename to src/event-builder/scroll.ts index 94334c2da..f94a30c86 100644 --- a/src/event-builder/scroll-view.ts +++ b/src/event-builder/scroll.ts @@ -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-input.ts b/src/event-builder/text-input.ts deleted file mode 100644 index f248219d2..000000000 --- a/src/event-builder/text-input.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { baseSyntheticEvent } from './base'; -import type { Size, TextRange } from './types'; - -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/event-builder/text.ts b/src/event-builder/text.ts new file mode 100644 index 000000000..9fb011f7a --- /dev/null +++ b/src/event-builder/text.ts @@ -0,0 +1,74 @@ +import { baseSyntheticEvent } from './base'; +import type { Size, TextRange } from './types'; + +/** + * 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 54a93f017..b49bf7092 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -8,7 +8,7 @@ import type { import type { Fiber, HostElement } from 'test-renderer'; import { act } from './act'; -import { EventBuilder } from './event-builder'; +import { buildScrollEvent, buildTouchEvent } from './event-builder'; import type { Point } from './event-builder/types'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; @@ -150,7 +150,7 @@ fireEvent.changeText = async (element: HostElement, text: string) => await fireEvent(element, 'changeText', text); fireEvent.press = async (element: HostElement, eventProps?: EventProps) => { - const event = EventBuilder.Common.touch(); + const event = buildTouchEvent(); if (eventProps) { mergeEventProps(event, eventProps); } @@ -159,7 +159,7 @@ fireEvent.press = async (element: HostElement, eventProps?: EventProps) => { }; fireEvent.scroll = async (element: HostElement, eventProps?: EventProps) => { - const event = EventBuilder.ScrollView.scroll(); + const event = buildScrollEvent(); if (eventProps) { mergeEventProps(event, eventProps); } diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts index 8a0d81841..1ce414e04 100644 --- a/src/user-event/clear.ts +++ b/src/user-event/clear.ts @@ -1,6 +1,11 @@ import type { HostElement } from 'test-renderer'; -import { EventBuilder } from '../event-builder'; +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'; @@ -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/paste.ts b/src/user-event/paste.ts index ad162255e..0eef523df 100644 --- a/src/user-event/paste.ts +++ b/src/user-event/paste.ts @@ -1,6 +1,13 @@ import type { HostElement } from 'test-renderer'; -import { EventBuilder } from '../event-builder'; +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'; @@ -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 99f1077f4..6d3b69cf1 100644 --- a/src/user-event/press/press.ts +++ b/src/user-event/press/press.ts @@ -1,7 +1,11 @@ import type { HostElement } from 'test-renderer'; import { act } from '../../act'; -import { EventBuilder } from '../../event-builder'; +import { + buildResponderGrantEvent, + buildResponderReleaseEvent, + buildTouchEvent, +} from '../../event-builder'; import { getEventHandlerFromProps } from '../../event-handler'; import { isHostElement } from '../../helpers/component-tree'; import { ErrorWithStack } from '../../helpers/errors'; @@ -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/scroll-to.ts b/src/user-event/scroll/scroll-to.ts index e68dc6277..ab22bd2e7 100644 --- a/src/user-event/scroll/scroll-to.ts +++ b/src/user-event/scroll/scroll-to.ts @@ -1,7 +1,7 @@ import { stringify } from 'jest-matcher-utils'; import type { HostElement } from 'test-renderer'; -import { EventBuilder } from '../../event-builder'; +import { buildScrollEvent } from '../../event-builder'; import type { Point, Size } from '../../event-builder/types'; import { ErrorWithStack } from '../../helpers/errors'; import { isHostScrollView } from '../../helpers/host-component-names'; @@ -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 3c92d18a6..c823ce75c 100644 --- a/src/user-event/type/type.ts +++ b/src/user-event/type/type.ts @@ -1,6 +1,16 @@ import type { HostElement } from 'test-renderer'; -import { EventBuilder } from '../../event-builder'; +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'; @@ -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 8e3232b42..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 () => { From 4fa58a71fa17e0ef8af6f1e8c73bb21631300a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:40:00 +0000 Subject: [PATCH 05/10] self code review --- src/event-builder/common.ts | 4 ++-- src/event-builder/types.ts | 23 ----------------------- src/fire-event.ts | 5 ++--- src/native-state.ts | 2 +- src/types.ts | 24 ++++++++++++++++++++++++ src/user-event/scroll/scroll-to.ts | 2 +- src/user-event/scroll/utils.ts | 2 +- src/user-event/utils/content-size.ts | 2 +- 8 files changed, 32 insertions(+), 32 deletions(-) delete mode 100644 src/event-builder/types.ts diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts index 4d030ab7b..43b6606ae 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -9,7 +9,7 @@ export function buildTouchEvent() { return { ...baseSyntheticEvent(), nativeEvent: { - changedTouches: [] as number[], + changedTouches: [] as unknown[], identifier: 0, locationX: 0, locationY: 0, @@ -17,7 +17,7 @@ export function buildTouchEvent() { pageY: 0, target: 0, timestamp: Date.now(), - touches: [] as number[], + touches: [] as unknown[], }, currentTarget: { measure: () => {} }, }; diff --git a/src/event-builder/types.ts b/src/event-builder/types.ts deleted file mode 100644 index ca7d30480..000000000 --- a/src/event-builder/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Location of an element. - */ -export interface Point { - y: number; - x: number; -} - -/** - * Size of an element. - */ -export interface Size { - height: number; - width: number; -} - -/** - * Range of text in a text input. - */ -export interface TextRange { - start: number; - end: number; -} diff --git a/src/fire-event.ts b/src/fire-event.ts index b49bf7092..e6f733642 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -9,7 +9,6 @@ import type { Fiber, HostElement } from 'test-renderer'; import { act } from './act'; import { buildScrollEvent, buildTouchEvent } from './event-builder'; -import type { Point } from './event-builder/types'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; import { isElementMounted } from './helpers/component-tree'; @@ -17,7 +16,7 @@ import { isHostScrollView, isHostTextInput } from './helpers/host-component-name import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; -import type { StringWithAutocomplete } from './types'; +import type { Point, StringWithAutocomplete } from './types'; function isTouchResponder(element: HostElement) { return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element); @@ -215,7 +214,7 @@ function mergeEventProps(target: Record, source: Record = { current: T; }; +/** + * Location of an element. + */ +export interface Point { + y: number; + x: number; +} + +/** + * Size of an element. + */ +export interface Size { + height: number; + 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/scroll/scroll-to.ts b/src/user-event/scroll/scroll-to.ts index ab22bd2e7..e561e1e4f 100644 --- a/src/user-event/scroll/scroll-to.ts +++ b/src/user-event/scroll/scroll-to.ts @@ -2,11 +2,11 @@ import { stringify } from 'jest-matcher-utils'; import type { HostElement } from 'test-renderer'; import { buildScrollEvent } from '../../event-builder'; -import type { Point, Size } from '../../event-builder/types'; 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 { createScrollSteps, inertialInterpolator, linearInterpolator } from './utils'; diff --git a/src/user-event/scroll/utils.ts b/src/user-event/scroll/utils.ts index 79afb3e51..8fd043edd 100644 --- a/src/user-event/scroll/utils.ts +++ b/src/user-event/scroll/utils.ts @@ -1,4 +1,4 @@ -import type { Point } from '../../event-builder/types'; +import type { Point } from '../../types'; const DEFAULT_STEPS_COUNT = 5; diff --git a/src/user-event/utils/content-size.ts b/src/user-event/utils/content-size.ts index 75354edaf..7d83a5673 100644 --- a/src/user-event/utils/content-size.ts +++ b/src/user-event/utils/content-size.ts @@ -1,4 +1,4 @@ -import type { Size } from '../../event-builder/types'; +import type { Size } from '../../types'; /** * Simple function for getting mock the size of given text. From c3f900df473b51a7078b7be469d5ddbf7a72ee68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:44:50 +0000 Subject: [PATCH 06/10] claude.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 CLAUDE.md 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 From 5ff241603ca45c77f2a65855a28c72a23106a917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:47:58 +0000 Subject: [PATCH 07/10] . --- src/event-builder/scroll.ts | 2 +- src/event-builder/text.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/event-builder/scroll.ts b/src/event-builder/scroll.ts index f94a30c86..610d1d956 100644 --- a/src/event-builder/scroll.ts +++ b/src/event-builder/scroll.ts @@ -1,5 +1,5 @@ +import type { Point, Size } from '../types'; import { baseSyntheticEvent } from './base'; -import type { Point, Size } from './types'; /** * Other options for constructing a scroll event. diff --git a/src/event-builder/text.ts b/src/event-builder/text.ts index 9fb011f7a..c8effcb74 100644 --- a/src/event-builder/text.ts +++ b/src/event-builder/text.ts @@ -1,5 +1,5 @@ +import type { Size, TextRange } from '../types'; import { baseSyntheticEvent } from './base'; -import type { Size, TextRange } from './types'; /** * Experimental values: From 62c47977d07f42692da310a4792358dcde3f37fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:54:29 +0000 Subject: [PATCH 08/10] code coverage --- src/event-builder/__tests__/common.test.ts | 57 ++++++++++++++++++++++ src/event-builder/__tests__/index.test.ts | 16 ++++++ src/event-builder/__tests__/scroll.test.ts | 31 ++++++++++++ src/event-builder/__tests__/text.test.ts | 47 ++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 src/event-builder/__tests__/common.test.ts create mode 100644 src/event-builder/__tests__/index.test.ts create mode 100644 src/event-builder/__tests__/scroll.test.ts create mode 100644 src/event-builder/__tests__/text.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..918e3d5f9 --- /dev/null +++ b/src/event-builder/__tests__/common.test.ts @@ -0,0 +1,57 @@ +import { + buildTouchEvent, + buildResponderGrantEvent, + buildResponderReleaseEvent, + buildFocusEvent, + buildBlurEvent, +} 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..88882e920 --- /dev/null +++ b/src/event-builder/__tests__/scroll.test.ts @@ -0,0 +1,31 @@ +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..e9d623ddb --- /dev/null +++ b/src/event-builder/__tests__/text.test.ts @@ -0,0 +1,47 @@ +import { + buildTextChangeEvent, + buildKeyPressEvent, + buildSubmitEditingEvent, + buildEndEditingEvent, + buildTextSelectionChangeEvent, + buildContentSizeChangeEvent, +} 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, + }); +}); From d002d24d9c93f34a1375118639c4ba738cc98c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 00:57:32 +0000 Subject: [PATCH 09/10] code cov --- src/__tests__/fire-event.test.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index ce4f6dd33..d3e2798ef 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -177,6 +177,19 @@ describe('fireEvent.scroll', () => { 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( From 45067f8a760acc777146a13dcc34b84c34a072ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 16 Feb 2026 11:42:18 +0000 Subject: [PATCH 10/10] format --- src/event-builder/__tests__/common.test.ts | 6 +++--- src/event-builder/__tests__/scroll.test.ts | 11 +++++++---- src/event-builder/__tests__/text.test.ts | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/event-builder/__tests__/common.test.ts b/src/event-builder/__tests__/common.test.ts index 918e3d5f9..a70e78332 100644 --- a/src/event-builder/__tests__/common.test.ts +++ b/src/event-builder/__tests__/common.test.ts @@ -1,9 +1,9 @@ import { - buildTouchEvent, + buildBlurEvent, + buildFocusEvent, buildResponderGrantEvent, buildResponderReleaseEvent, - buildFocusEvent, - buildBlurEvent, + buildTouchEvent, } from '../common'; test('buildTouchEvent returns event with touch nativeEvent', () => { diff --git a/src/event-builder/__tests__/scroll.test.ts b/src/event-builder/__tests__/scroll.test.ts index 88882e920..90363934c 100644 --- a/src/event-builder/__tests__/scroll.test.ts +++ b/src/event-builder/__tests__/scroll.test.ts @@ -21,10 +21,13 @@ test('buildScrollEvent uses provided offset', () => { }); test('buildScrollEvent uses provided options', () => { - const event = buildScrollEvent({ y: 0, x: 0 }, { - contentSize: { height: 1000, width: 400 }, - layoutMeasurement: { height: 800, width: 400 }, - }); + 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 index e9d623ddb..98b5f6bdd 100644 --- a/src/event-builder/__tests__/text.test.ts +++ b/src/event-builder/__tests__/text.test.ts @@ -1,10 +1,10 @@ import { - buildTextChangeEvent, + buildContentSizeChangeEvent, + buildEndEditingEvent, buildKeyPressEvent, buildSubmitEditingEvent, - buildEndEditingEvent, + buildTextChangeEvent, buildTextSelectionChangeEvent, - buildContentSizeChangeEvent, } from '../text'; test('buildTextChangeEvent returns event with text', () => {