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/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 52a036384..1fb1a62d8 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,30 @@ await render( 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, + 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/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, 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..614d9c370 100644 --- a/src/event-builder/common.ts +++ b/src/event-builder/common.ts @@ -82,3 +82,30 @@ 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. + */ +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..93a646ac7 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,31 @@ await render( 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, + 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/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,