From 86b492af1f99c758700806931f24b176343649a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 15:25:04 +0200 Subject: [PATCH 1/4] feat: fireEvent.layout --- docs/api/fire-event.md | 26 ++++++++- src/__tests__/fire-event.test.tsx | 58 +++++++++++++++++++ src/event-builder/__tests__/common.test.ts | 11 ++++ src/event-builder/common.ts | 31 ++++++++++ src/fire-event.ts | 7 ++- .../docs/14.x/docs/api/events/fire-event.mdx | 26 ++++++++- 6 files changed, 156 insertions(+), 3 deletions(-) diff --git a/docs/api/fire-event.md b/docs/api/fire-event.md index 52a036384..2da3155e9 100644 --- a/docs/api/fire-event.md +++ b/docs/api/fire-event.md @@ -56,7 +56,7 @@ await render( await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur'); ``` -FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`. +FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`, `layout`. ### `fireEvent.press` @@ -163,3 +163,27 @@ await render( await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData); ``` + +### `fireEvent.layout` + +```tsx +fireEvent.layout: ( + instance: TestInstance, + layout?: Partial<{ x: number; y: number; width: number; height: number }>, +) => Promise +``` + +Builds a layout event carrying the given `layout` rectangle and invokes the `layout` handler on the element or nearest eligible parent. Use it to simulate the layout engine measuring an element, e.g. to test components that adapt to a measured size. + +The `layout` values are merged onto a zeroed rectangle (`{ x: 0, y: 0, width: 0, height: 0 }`), so pass only the fields your component reads. + +```jsx +import { View } from 'react-native'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +const onLayoutMock = jest.fn(); + +await render(); + +await fireEvent.layout(screen.getByTestId('box'), { width: 320, height: 80 }); +``` diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index 0f99ff98d..ea39223ac 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -425,6 +425,64 @@ describe('fireEvent.scroll', () => { }); }); +describe('fireEvent.layout', () => { + test('passes default layout event object to handler', async () => { + const onLayout = jest.fn(); + await render(); + + await fireEvent.layout(screen.getByTestId('view')); + + expect(onLayout.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "layout": { + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "target": 0, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + } + `); + }); + + test('merges the passed layout onto the zeroed rectangle', async () => { + const onLayout = jest.fn(); + await render(); + + await fireEvent.layout(screen.getByTestId('view'), { width: 200, height: 80 }); + + expect(onLayout.mock.calls[0][0].nativeEvent).toEqual({ + layout: { x: 0, y: 0, width: 200, height: 80 }, + target: 0, + }); + }); + + test('bubbles up to find the handler on an ancestor element', async () => { + const onLayout = jest.fn(); + await render( + + Content + , + ); + + await fireEvent.layout(screen.getByText('Content'), { height: 80 }); + + expect(onLayout).toHaveBeenCalledTimes(1); + expect(onLayout.mock.calls[0][0].nativeEvent.layout.height).toBe(80); + }); +}); + test('fireEvent fires custom event (onCustomEvent) on composite component', async () => { const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => ( onCustomEvent('event data')}> diff --git a/src/event-builder/__tests__/common.test.ts b/src/event-builder/__tests__/common.test.ts index a70e78332..42066a66d 100644 --- a/src/event-builder/__tests__/common.test.ts +++ b/src/event-builder/__tests__/common.test.ts @@ -1,6 +1,7 @@ import { buildBlurEvent, buildFocusEvent, + buildLayoutEvent, buildResponderGrantEvent, buildResponderReleaseEvent, buildTouchEvent, @@ -55,3 +56,13 @@ test('buildBlurEvent returns event with target', () => { expect(event.nativeEvent).toEqual({ target: 0 }); expect(event).toHaveProperty('preventDefault'); }); + +test('buildLayoutEvent returns event with zeroed layout rectangle', () => { + const event = buildLayoutEvent(); + + expect(event.nativeEvent).toEqual({ + layout: { x: 0, y: 0, width: 0, height: 0 }, + target: 0, + }); + expect(event).toHaveProperty('preventDefault'); +}); diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts index 9224faa0c..f093c3429 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -82,3 +82,34 @@ export function buildAccessibilityActionEvent(actionName: string) { }, }; } + +/** + * Layout rectangle of an element, as measured by the layout engine. + */ +export interface LayoutRectangle { + x: number; + y: number; + width: number; + height: number; +} + +/** + * Builds a layout event, as delivered to the `onLayout` handler when an element's + * size or position is measured by the layout engine. + * + * The passed `layout` values are merged onto a zeroed rectangle, so only the + * fields relevant to the test need to be provided. + * + * Experimental values: + * - iOS: `{"layout": {"height": 100, "width": 200, "x": 0, "y": 0}, "target": 75}` + * - Android: `{"layout": {"height": 100, "width": 200, "x": 0, "y": 0}, "target": 53}` + */ +export function buildLayoutEvent(layout?: Partial) { + return { + ...baseSyntheticEvent(), + nativeEvent: { + layout: { x: 0, y: 0, width: 0, height: 0, ...layout }, + target: 0, + }, + }; +} diff --git a/src/fire-event.ts b/src/fire-event.ts index 88991376c..4b64b652c 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -8,7 +8,8 @@ import type { import type { Fiber, TestInstance } from 'test-renderer'; import { act } from './act'; -import { buildScrollEvent, buildTouchEvent } from './event-builder'; +import type { LayoutRectangle } from './event-builder'; +import { buildLayoutEvent, buildScrollEvent, buildTouchEvent } from './event-builder'; import type { EventHandler } from './event-handler'; import { getEventHandlerFromProps } from './event-handler'; import { isInstanceMounted } from './helpers/component-tree'; @@ -168,6 +169,10 @@ fireEvent.scroll = async (instance: TestInstance, eventProps?: EventProps) => { await fireEvent(instance, 'scroll', event); }; +fireEvent.layout = async (instance: TestInstance, layout?: Partial) => { + await fireEvent(instance, 'layout', buildLayoutEvent(layout)); +}; + export { fireEvent }; const scrollEventNames = new Set([ diff --git a/website/docs/14.x/docs/api/events/fire-event.mdx b/website/docs/14.x/docs/api/events/fire-event.mdx index 033441fae..2b939a238 100644 --- a/website/docs/14.x/docs/api/events/fire-event.mdx +++ b/website/docs/14.x/docs/api/events/fire-event.mdx @@ -58,7 +58,7 @@ await render( await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur'); ``` -FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`. +FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`, `layout`. ### `fireEvent.press` {#press} @@ -168,3 +168,27 @@ await render( await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData); ``` + +### `fireEvent.layout` {#layout} + +```tsx +fireEvent.layout: ( + instance: TestInstance, + layout?: Partial<{ x: number; y: number; width: number; height: number }>, +) => Promise +``` + +Builds a layout event carrying the given `layout` rectangle and invokes the `layout` handler on the element or nearest eligible parent. Use it to simulate the layout engine measuring an element, e.g. to test components that adapt to a measured size. + +The `layout` values are merged onto a zeroed rectangle (`{ x: 0, y: 0, width: 0, height: 0 }`), so pass only the fields your component reads. + +```jsx +import { View } from 'react-native'; +import { render, screen, fireEvent } from '@testing-library/react-native'; + +const onLayoutMock = jest.fn(); + +await render(); + +await fireEvent.layout(screen.getByTestId('box'), { width: 320, height: 80 }); +``` From 14da6fc46d7aaf983eb128a1f9ec05f34b7e6ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 15:38:20 +0200 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 9 +++++++++ website/docs/14.x/docs/api/events/fire-event.mdx | 4 ++++ website/docs/14.x/docs/api/events/user-event.mdx | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a43137ce..baea67f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to React Native Testing Library will be documented in this file, starting with v14. +## 14.1.0 + +### Features + +- Added `fireEvent.layout()` to simulate the layout engine measuring an element, invoking the + `onLayout` handler with a synthetic layout event. +- Added `userEvent.accessibilityAction()` to dispatch a named accessibility action to an + element, invoking its `onAccessibilityAction` handler. + ## 14.0.0 ### Migration guide diff --git a/website/docs/14.x/docs/api/events/fire-event.mdx b/website/docs/14.x/docs/api/events/fire-event.mdx index 2b939a238..93a646ac7 100644 --- a/website/docs/14.x/docs/api/events/fire-event.mdx +++ b/website/docs/14.x/docs/api/events/fire-event.mdx @@ -171,6 +171,10 @@ await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData); ### `fireEvent.layout` {#layout} +:::note +Available since React Native Testing Library 14.1.0. +::: + ```tsx fireEvent.layout: ( instance: TestInstance, diff --git a/website/docs/14.x/docs/api/events/user-event.mdx b/website/docs/14.x/docs/api/events/user-event.mdx index ce9b153b7..4ecdbe85e 100644 --- a/website/docs/14.x/docs/api/events/user-event.mdx +++ b/website/docs/14.x/docs/api/events/user-event.mdx @@ -297,6 +297,10 @@ The sequence of events depends on whether the scroll includes an optional moment ## `accessibilityAction()` +:::note +Available since React Native Testing Library 14.1.0. +::: + ```ts accessibilityAction( instance: TestInstance, From 4da596718eea6d5b935d13cf38cd1a43dd7f824e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 15:40:28 +0200 Subject: [PATCH 3/4] . --- src/event-builder/common.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/event-builder/common.ts b/src/event-builder/common.ts index f093c3429..614d9c370 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -99,10 +99,6 @@ export interface LayoutRectangle { * * The passed `layout` values are merged onto a zeroed rectangle, so only the * fields relevant to the test need to be provided. - * - * Experimental values: - * - iOS: `{"layout": {"height": 100, "width": 200, "x": 0, "y": 0}, "target": 75}` - * - Android: `{"layout": {"height": 100, "width": 200, "x": 0, "y": 0}, "target": 53}` */ export function buildLayoutEvent(layout?: Partial) { return { From e4aba40e44dd307c179f5bb6ce4d424d72709d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 21 Jul 2026 15:44:56 +0200 Subject: [PATCH 4/4] agent docs --- agents/build-and-validation.md | 10 ++++++++++ docs/api/fire-event.md | 3 +++ docs/api/user-event.md | 3 +++ 3 files changed, 16 insertions(+) diff --git a/agents/build-and-validation.md b/agents/build-and-validation.md index 675036dc8..e2dd812ef 100644 --- a/agents/build-and-validation.md +++ b/agents/build-and-validation.md @@ -10,6 +10,8 @@ - Check formatting: `yarn format:check` - Validate the main package: `yarn validate` - Build the package: `yarn build` +- Regenerate package docs: `yarn docs:generate` +- Check package docs are in sync: `yarn docs:check` ## Command notes @@ -17,6 +19,14 @@ - `yarn validate` runs typecheck, tests, lint, and oxfmt checks for the main package. - `yarn build` cleans `dist/`, transpiles source with Babel, and emits TypeScript declarations. +## Documentation + +- The docs under `website/docs/` are the source of truth. The `docs/api/*.md` files are + generated from them — never edit those by hand. +- After making any documentation changes, run `yarn docs:generate` to regenerate the package + docs, and commit the regenerated files alongside your `website/` edits. `yarn docs:check` + (part of `validate:all`) fails if they are out of sync. + ## Repo layout - `src/`: source code diff --git a/docs/api/fire-event.md b/docs/api/fire-event.md index 2da3155e9..1fb1a62d8 100644 --- a/docs/api/fire-event.md +++ b/docs/api/fire-event.md @@ -166,6 +166,9 @@ await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData); ### `fireEvent.layout` +> [!NOTE] +> Available since React Native Testing Library 14.1.0. + ```tsx fireEvent.layout: ( instance: TestInstance, diff --git a/docs/api/user-event.md b/docs/api/user-event.md index 7991ec3f3..234ec5b5e 100644 --- a/docs/api/user-event.md +++ b/docs/api/user-event.md @@ -296,6 +296,9 @@ The sequence of events depends on whether the scroll includes an optional moment ## `accessibilityAction()` +> [!NOTE] +> Available since React Native Testing Library 14.1.0. + ```ts accessibilityAction( instance: TestInstance,