From c23f2a4ef12c204782877b8e27f8ff1ebd427df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 11:09:00 +0100 Subject: [PATCH 1/4] user-event/base tests --- AGENTS.md | 1 + .../event-builder/__tests__/base.test.ts | 35 +++++++++++++++++++ src/user-event/event-builder/base.ts | 8 +++-- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/user-event/event-builder/__tests__/base.test.ts diff --git a/AGENTS.md b/AGENTS.md index 52cde746f..eb9c00d30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ The project uses `yarn` for dependency management and script execution. - **Location:** Tests are located within `src`, typically co-located in `__tests__` directories. - **Setup:** `jest-setup.ts` configures the test environment. `src/index.ts` automatically configures cleanup after each test unless skipped. - **Coverage:** Collected from `src`, excluding tests. + - **Organization:** Use `describe` to group test by theme. Avoid putting all tests in the same `describe` block. Avoid `describe` nesting. Avoid `describe` with only single test, make that test top-level. Prefere `test` over `it`. - **Commits & Releases:** - **Commits:** Follow the **Conventional Commits** specification (e.g., `fix:`, `feat:`, `chore:`). This is enforced and used for changelog generation. diff --git a/src/user-event/event-builder/__tests__/base.test.ts b/src/user-event/event-builder/__tests__/base.test.ts new file mode 100644 index 000000000..47ecedc24 --- /dev/null +++ b/src/user-event/event-builder/__tests__/base.test.ts @@ -0,0 +1,35 @@ +import { baseSyntheticEvent } from '../base'; + +test('returns object with all required properties and default values', () => { + const event = baseSyntheticEvent(); + + expect(event.currentTarget).toEqual({}); + expect(event.target).toEqual({}); + expect(event.timeStamp).toBe(0); + expect(event.isDefaultPrevented?.()).toBe(false); + expect(event.isPropagationStopped?.()).toBe(false); + expect(event.isPersistent?.()).toBe(false); + expect(typeof event.stopPropagation).toBe('function'); + expect(typeof event.preventDefault).toBe('function'); + expect(typeof event.persist).toBe('function'); +}); + +test('returns a new object instance on each call', () => { + const event1 = baseSyntheticEvent(); + const event2 = baseSyntheticEvent(); + + expect(event1).not.toBe(event2); + expect(event1.currentTarget).not.toBe(event2.currentTarget); + expect(event1.target).not.toBe(event2.target); +}); + +test('can be spread into other objects', () => { + const extendedEvent = { + ...baseSyntheticEvent(), + nativeEvent: { test: 'value' }, + }; + + expect(extendedEvent).toHaveProperty('currentTarget'); + expect(extendedEvent).toHaveProperty('preventDefault'); + expect(extendedEvent.nativeEvent).toEqual({ test: 'value' }); +}); diff --git a/src/user-event/event-builder/base.ts b/src/user-event/event-builder/base.ts index 46da078d7..ff45e9828 100644 --- a/src/user-event/event-builder/base.ts +++ b/src/user-event/event-builder/base.ts @@ -1,7 +1,12 @@ import type { BaseSyntheticEvent } from 'react'; /** Builds base syntentic event stub, with prop values as inspected in RN runtime. */ -export function baseSyntheticEvent(): Partial> { +type BaseEvent = Partial> & { + // `isPersistent` is not a standard prop, but it's used in RN runtime. See: https://react.dev/reference/react-dom/components/common#react-event-object-methods + isPersistent: () => boolean; +}; + +export function baseSyntheticEvent(): BaseEvent { return { currentTarget: {}, target: {}, @@ -10,7 +15,6 @@ export function baseSyntheticEvent(): Partial {}, isPropagationStopped: () => false, persist: () => {}, - // @ts-expect-error: `isPersistent` is not a standard prop, but it's used in RN runtime. See: https://react.dev/reference/react-dom/components/common#react-event-object-methods isPersistent: () => false, timeStamp: 0, }; From aee8399c11075904345d6d100a1934de2af21b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 11:17:10 +0100 Subject: [PATCH 2/4] errors tests --- src/helpers/__tests__/errors.test.ts | 80 ++++++++++++++++++++++++++++ src/helpers/errors.ts | 40 -------------- 2 files changed, 80 insertions(+), 40 deletions(-) create mode 100644 src/helpers/__tests__/errors.test.ts diff --git a/src/helpers/__tests__/errors.test.ts b/src/helpers/__tests__/errors.test.ts new file mode 100644 index 000000000..8c46fb0a9 --- /dev/null +++ b/src/helpers/__tests__/errors.test.ts @@ -0,0 +1,80 @@ +import { ErrorWithStack, copyStackTrace } from '../errors'; + +describe('ErrorWithStack', () => { + test('should create an error with message and handle stack trace capture', () => { + const error = new ErrorWithStack('Test error', ErrorWithStack); + expect(error.message).toBe('Test error'); + expect(error).toBeInstanceOf(Error); + }); + + test('should capture stack trace if Error.captureStackTrace is available', () => { + const originalCaptureStackTrace = Error.captureStackTrace; + const captureStackTraceSpy = jest.fn(); + Error.captureStackTrace = captureStackTraceSpy; + + const error = new ErrorWithStack('Test error', ErrorWithStack); + expect(captureStackTraceSpy).toHaveBeenCalledWith(error, ErrorWithStack); + + Error.captureStackTrace = originalCaptureStackTrace; + }); + + test('should work when Error.captureStackTrace is not available', () => { + const originalCaptureStackTrace = Error.captureStackTrace; + // @ts-expect-error - intentionally removing captureStackTrace + delete Error.captureStackTrace; + + const error = new ErrorWithStack('Test error', ErrorWithStack); + expect(error.message).toBe('Test error'); + expect(error).toBeInstanceOf(Error); + + Error.captureStackTrace = originalCaptureStackTrace; + }); +}); + +describe('copyStackTrace', () => { + test('should copy stack trace from source to target when both are Error instances', () => { + const target = new Error('Target error'); + const source = new Error('Source error'); + source.stack = 'Error: Source error\n at test.js:1:1'; + + copyStackTrace(target, source); + + expect(target.stack).toBe('Error: Target error\n at test.js:1:1'); + }); + + test('should handle stack trace with multiple occurrences of source message', () => { + const target = new Error('Target error'); + const source = new Error('Source error'); + source.stack = + 'Error: Source error\n at test.js:1:1\nError: Source error\n at test.js:2:2'; + + copyStackTrace(target, source); + + // Should replace only the first occurrence + expect(target.stack).toBe( + 'Error: Target error\n at test.js:1:1\nError: Source error\n at test.js:2:2', + ); + }); + + test('should not modify target when conditions are not met', () => { + const targetNotError = { message: 'Not an error' }; + const source = new Error('Source error'); + source.stack = 'Error: Source error\n at test.js:1:1'; + + copyStackTrace(targetNotError, source); + expect(targetNotError).toEqual({ message: 'Not an error' }); + + const target = new Error('Target error'); + const originalStack = target.stack; + const sourceNotError = { message: 'Not an error' }; + + copyStackTrace(target, sourceNotError as Error); + expect(target.stack).toBe(originalStack); + + const sourceNoStack = new Error('Source error'); + delete sourceNoStack.stack; + + copyStackTrace(target, sourceNoStack); + expect(target.stack).toBe(originalStack); + }); +}); diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 9ceb8ce0b..a6965619c 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,5 +1,3 @@ -import prettyFormat from 'pretty-format'; - export class ErrorWithStack extends Error { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type constructor(message: string | undefined, callsite: Function) { @@ -10,44 +8,6 @@ export class ErrorWithStack extends Error { } } -export const prepareErrorMessage = ( - // TS states that error caught in a catch close are of type `unknown` - // most real cases will be `Error`, but better safe than sorry - error: unknown, - name?: string, - value?: unknown, -): string => { - let errorMessage: string; - if (error instanceof Error) { - // Strip info about custom predicate - errorMessage = error.message.replace(/ matching custom predicate[^]*/gm, ''); - } else if (error && typeof error === 'object') { - errorMessage = error.toString(); - } else { - errorMessage = 'Caught unknown error'; - } - - if (name && value) { - errorMessage += ` with ${name} ${prettyFormat(value, { min: true })}`; - } - return errorMessage; -}; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export const createQueryByError = (error: unknown, callsite: Function): null => { - if (error instanceof Error) { - if (error.message.includes('No instances found')) { - return null; - } - throw new ErrorWithStack(error.message, callsite); - } - - throw new ErrorWithStack( - `Query: caught unknown error type: ${typeof error}, value: ${error}`, - callsite, - ); -}; - export function copyStackTrace(target: unknown, stackTraceSource: Error) { if (target instanceof Error && stackTraceSource.stack) { target.stack = stackTraceSource.stack.replace(stackTraceSource.message, target.message); From 98752361fc1f16a795c95c16443d66775070db58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 11:18:59 +0100 Subject: [PATCH 3/4] . --- src/helpers/__tests__/errors.test.ts | 38 ++++++++++++---------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/helpers/__tests__/errors.test.ts b/src/helpers/__tests__/errors.test.ts index 8c46fb0a9..09fc7abe7 100644 --- a/src/helpers/__tests__/errors.test.ts +++ b/src/helpers/__tests__/errors.test.ts @@ -1,31 +1,29 @@ -import { ErrorWithStack, copyStackTrace } from '../errors'; +import { copyStackTrace, ErrorWithStack } from '../errors'; describe('ErrorWithStack', () => { - test('should create an error with message and handle stack trace capture', () => { + test('should create an error with message', () => { const error = new ErrorWithStack('Test error', ErrorWithStack); expect(error.message).toBe('Test error'); expect(error).toBeInstanceOf(Error); - }); - test('should capture stack trace if Error.captureStackTrace is available', () => { const originalCaptureStackTrace = Error.captureStackTrace; - const captureStackTraceSpy = jest.fn(); - Error.captureStackTrace = captureStackTraceSpy; + // @ts-expect-error - intentionally removing captureStackTrace + delete Error.captureStackTrace; - const error = new ErrorWithStack('Test error', ErrorWithStack); - expect(captureStackTraceSpy).toHaveBeenCalledWith(error, ErrorWithStack); + const errorWithoutCapture = new ErrorWithStack('Test error', ErrorWithStack); + expect(errorWithoutCapture.message).toBe('Test error'); + expect(errorWithoutCapture).toBeInstanceOf(Error); Error.captureStackTrace = originalCaptureStackTrace; }); - test('should work when Error.captureStackTrace is not available', () => { + test('should capture stack trace if Error.captureStackTrace is available', () => { const originalCaptureStackTrace = Error.captureStackTrace; - // @ts-expect-error - intentionally removing captureStackTrace - delete Error.captureStackTrace; + const captureStackTraceSpy = jest.fn(); + Error.captureStackTrace = captureStackTraceSpy; const error = new ErrorWithStack('Test error', ErrorWithStack); - expect(error.message).toBe('Test error'); - expect(error).toBeInstanceOf(Error); + expect(captureStackTraceSpy).toHaveBeenCalledWith(error, ErrorWithStack); Error.captureStackTrace = originalCaptureStackTrace; }); @@ -38,20 +36,16 @@ describe('copyStackTrace', () => { source.stack = 'Error: Source error\n at test.js:1:1'; copyStackTrace(target, source); - expect(target.stack).toBe('Error: Target error\n at test.js:1:1'); - }); - test('should handle stack trace with multiple occurrences of source message', () => { - const target = new Error('Target error'); - const source = new Error('Source error'); - source.stack = + const target2 = new Error('Target error'); + const source2 = new Error('Source error'); + source2.stack = 'Error: Source error\n at test.js:1:1\nError: Source error\n at test.js:2:2'; - copyStackTrace(target, source); - + copyStackTrace(target2, source2); // Should replace only the first occurrence - expect(target.stack).toBe( + expect(target2.stack).toBe( 'Error: Target error\n at test.js:1:1\nError: Source error\n at test.js:2:2', ); }); From 5457b24958817eec69136fc749802bcae5ad44b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 11 Jan 2026 11:24:40 +0100 Subject: [PATCH 4/4] dead code analysis --- src/event-handler.ts | 2 +- src/fire-event.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/event-handler.ts b/src/event-handler.ts index f2c6ec83e..67c4e2044 100644 --- a/src/event-handler.ts +++ b/src/event-handler.ts @@ -30,7 +30,7 @@ export function getEventHandlerFromProps( return undefined; } -export function getEventHandlerName(eventName: string) { +function getEventHandlerName(eventName: string) { return `on${capitalizeFirstLetter(eventName)}`; } diff --git a/src/fire-event.ts b/src/fire-event.ts index 0a1a176f6..ba4914ffe 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -17,7 +17,7 @@ import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; -export function isTouchResponder(element: HostElement) { +function isTouchResponder(element: HostElement) { return Boolean(element.props.onStartShouldSetResponder) || isHostTextInput(element); } @@ -44,7 +44,7 @@ const textInputEventsIgnoringEditableProp = new Set([ 'onScroll', ]); -export function isEventEnabled( +function isEventEnabled( element: HostElement, eventName: string, nearestTouchResponder?: HostElement,