Skip to content

Commit c1978b3

Browse files
feat: fireEvent.layout (#1931)
1 parent 34c731d commit c1978b3

10 files changed

Lines changed: 185 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
All notable changes to React Native Testing Library will be documented in this file, starting
44
with v14.
55

6+
## 14.1.0
7+
8+
### Features
9+
10+
- Added `fireEvent.layout()` to simulate the layout engine measuring an element, invoking the
11+
`onLayout` handler with a synthetic layout event.
12+
- Added `userEvent.accessibilityAction()` to dispatch a named accessibility action to an
13+
element, invoking its `onAccessibilityAction` handler.
14+
615
## 14.0.0
716

817
### Migration guide

‎agents/build-and-validation.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@
1010
- Check formatting: `yarn format:check`
1111
- Validate the main package: `yarn validate`
1212
- Build the package: `yarn build`
13+
- Regenerate package docs: `yarn docs:generate`
14+
- Check package docs are in sync: `yarn docs:check`
1315

1416
## Command notes
1517

1618
- `yarn lint` runs ESLint on `src`.
1719
- `yarn validate` runs typecheck, tests, lint, and oxfmt checks for the main package.
1820
- `yarn build` cleans `dist/`, transpiles source with Babel, and emits TypeScript declarations.
1921

22+
## Documentation
23+
24+
- The docs under `website/docs/` are the source of truth. The `docs/api/*.md` files are
25+
generated from them — never edit those by hand.
26+
- After making any documentation changes, run `yarn docs:generate` to regenerate the package
27+
docs, and commit the regenerated files alongside your `website/` edits. `yarn docs:check`
28+
(part of `validate:all`) fails if they are out of sync.
29+
2030
## Repo layout
2131

2232
- `src/`: source code

‎docs/api/fire-event.md‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ await render(
5656
await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
5757
```
5858

59-
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`.
59+
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`, `layout`.
6060

6161
### `fireEvent.press`
6262

@@ -163,3 +163,30 @@ await render(
163163

164164
await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData);
165165
```
166+
167+
### `fireEvent.layout`
168+
169+
> [!NOTE]
170+
> Available since React Native Testing Library 14.1.0.
171+
172+
```tsx
173+
fireEvent.layout: (
174+
instance: TestInstance,
175+
layout?: Partial<{ x: number; y: number; width: number; height: number }>,
176+
) => Promise<void>
177+
```
178+
179+
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.
180+
181+
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.
182+
183+
```jsx
184+
import { View } from 'react-native';
185+
import { render, screen, fireEvent } from '@testing-library/react-native';
186+
187+
const onLayoutMock = jest.fn();
188+
189+
await render(<View testID="box" onLayout={onLayoutMock} />);
190+
191+
await fireEvent.layout(screen.getByTestId('box'), { width: 320, height: 80 });
192+
```

‎docs/api/user-event.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ The sequence of events depends on whether the scroll includes an optional moment
296296

297297
## `accessibilityAction()`
298298

299+
> [!NOTE]
300+
> Available since React Native Testing Library 14.1.0.
301+
299302
```ts
300303
accessibilityAction(
301304
instance: TestInstance,

‎src/__tests__/fire-event.test.tsx‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,64 @@ describe('fireEvent.scroll', () => {
425425
});
426426
});
427427

428+
describe('fireEvent.layout', () => {
429+
test('passes default layout event object to handler', async () => {
430+
const onLayout = jest.fn();
431+
await render(<View testID="view" onLayout={onLayout} />);
432+
433+
await fireEvent.layout(screen.getByTestId('view'));
434+
435+
expect(onLayout.mock.calls[0][0]).toMatchInlineSnapshot(`
436+
{
437+
"currentTarget": {},
438+
"isDefaultPrevented": [Function],
439+
"isPersistent": [Function],
440+
"isPropagationStopped": [Function],
441+
"nativeEvent": {
442+
"layout": {
443+
"height": 0,
444+
"width": 0,
445+
"x": 0,
446+
"y": 0,
447+
},
448+
"target": 0,
449+
},
450+
"persist": [Function],
451+
"preventDefault": [Function],
452+
"stopPropagation": [Function],
453+
"target": {},
454+
"timeStamp": 0,
455+
}
456+
`);
457+
});
458+
459+
test('merges the passed layout onto the zeroed rectangle', async () => {
460+
const onLayout = jest.fn();
461+
await render(<View testID="view" onLayout={onLayout} />);
462+
463+
await fireEvent.layout(screen.getByTestId('view'), { width: 200, height: 80 });
464+
465+
expect(onLayout.mock.calls[0][0].nativeEvent).toEqual({
466+
layout: { x: 0, y: 0, width: 200, height: 80 },
467+
target: 0,
468+
});
469+
});
470+
471+
test('bubbles up to find the handler on an ancestor element', async () => {
472+
const onLayout = jest.fn();
473+
await render(
474+
<View testID="view" onLayout={onLayout}>
475+
<Text>Content</Text>
476+
</View>,
477+
);
478+
479+
await fireEvent.layout(screen.getByText('Content'), { height: 80 });
480+
481+
expect(onLayout).toHaveBeenCalledTimes(1);
482+
expect(onLayout.mock.calls[0][0].nativeEvent.layout.height).toBe(80);
483+
});
484+
});
485+
428486
test('fireEvent fires custom event (onCustomEvent) on composite component', async () => {
429487
const CustomComponent = ({ onCustomEvent }: { onCustomEvent: (data: string) => void }) => (
430488
<TouchableOpacity onPress={() => onCustomEvent('event data')}>

‎src/event-builder/__tests__/common.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
buildBlurEvent,
33
buildFocusEvent,
4+
buildLayoutEvent,
45
buildResponderGrantEvent,
56
buildResponderReleaseEvent,
67
buildTouchEvent,
@@ -55,3 +56,13 @@ test('buildBlurEvent returns event with target', () => {
5556
expect(event.nativeEvent).toEqual({ target: 0 });
5657
expect(event).toHaveProperty('preventDefault');
5758
});
59+
60+
test('buildLayoutEvent returns event with zeroed layout rectangle', () => {
61+
const event = buildLayoutEvent();
62+
63+
expect(event.nativeEvent).toEqual({
64+
layout: { x: 0, y: 0, width: 0, height: 0 },
65+
target: 0,
66+
});
67+
expect(event).toHaveProperty('preventDefault');
68+
});

‎src/event-builder/common.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,30 @@ export function buildAccessibilityActionEvent(actionName: string) {
8282
},
8383
};
8484
}
85+
86+
/**
87+
* Layout rectangle of an element, as measured by the layout engine.
88+
*/
89+
export interface LayoutRectangle {
90+
x: number;
91+
y: number;
92+
width: number;
93+
height: number;
94+
}
95+
96+
/**
97+
* Builds a layout event, as delivered to the `onLayout` handler when an element's
98+
* size or position is measured by the layout engine.
99+
*
100+
* The passed `layout` values are merged onto a zeroed rectangle, so only the
101+
* fields relevant to the test need to be provided.
102+
*/
103+
export function buildLayoutEvent(layout?: Partial<LayoutRectangle>) {
104+
return {
105+
...baseSyntheticEvent(),
106+
nativeEvent: {
107+
layout: { x: 0, y: 0, width: 0, height: 0, ...layout },
108+
target: 0,
109+
},
110+
};
111+
}

‎src/fire-event.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
import type { Fiber, TestInstance } from 'test-renderer';
99

1010
import { act } from './act';
11-
import { buildScrollEvent, buildTouchEvent } from './event-builder';
11+
import type { LayoutRectangle } from './event-builder';
12+
import { buildLayoutEvent, buildScrollEvent, buildTouchEvent } from './event-builder';
1213
import type { EventHandler } from './event-handler';
1314
import { getEventHandlerFromProps } from './event-handler';
1415
import { isInstanceMounted } from './helpers/component-tree';
@@ -168,6 +169,10 @@ fireEvent.scroll = async (instance: TestInstance, eventProps?: EventProps) => {
168169
await fireEvent(instance, 'scroll', event);
169170
};
170171

172+
fireEvent.layout = async (instance: TestInstance, layout?: Partial<LayoutRectangle>) => {
173+
await fireEvent(instance, 'layout', buildLayoutEvent(layout));
174+
};
175+
171176
export { fireEvent };
172177

173178
const scrollEventNames = new Set([

‎website/docs/14.x/docs/api/events/fire-event.mdx‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ await render(
5858
await fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
5959
```
6060

61-
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`.
61+
FireEvent exposes convenience methods for common events like: `press`, `changeText`, `scroll`, `layout`.
6262

6363
### `fireEvent.press` {#press}
6464

@@ -168,3 +168,31 @@ await render(
168168

169169
await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData);
170170
```
171+
172+
### `fireEvent.layout` {#layout}
173+
174+
:::note
175+
Available since React Native Testing Library 14.1.0.
176+
:::
177+
178+
```tsx
179+
fireEvent.layout: (
180+
instance: TestInstance,
181+
layout?: Partial<{ x: number; y: number; width: number; height: number }>,
182+
) => Promise<void>
183+
```
184+
185+
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.
186+
187+
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.
188+
189+
```jsx
190+
import { View } from 'react-native';
191+
import { render, screen, fireEvent } from '@testing-library/react-native';
192+
193+
const onLayoutMock = jest.fn();
194+
195+
await render(<View testID="box" onLayout={onLayoutMock} />);
196+
197+
await fireEvent.layout(screen.getByTestId('box'), { width: 320, height: 80 });
198+
```

‎website/docs/14.x/docs/api/events/user-event.mdx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ The sequence of events depends on whether the scroll includes an optional moment
297297

298298
## `accessibilityAction()`
299299

300+
:::note
301+
Available since React Native Testing Library 14.1.0.
302+
:::
303+
300304
```ts
301305
accessibilityAction(
302306
instance: TestInstance,

0 commit comments

Comments
 (0)