Skip to content

Commit e6d4ac9

Browse files
committed
feat: User Event pullToRefresh()
1 parent c1978b3 commit e6d4ac9

5 files changed

Lines changed: 149 additions & 2 deletions

File tree

docs/api/user-event.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,30 @@ The sequence of events depends on whether the scroll includes an optional moment
294294
- `scroll` (multiple events)
295295
- `momentumScrollEnd`
296296

297+
## `pullToRefresh()`
298+
299+
> [!NOTE]
300+
> Available since React Native Testing Library 14.1.0.
301+
302+
```ts
303+
pullToRefresh(
304+
instance: TestInstance,
305+
): Promise<void>
306+
```
307+
308+
Example
309+
310+
```ts
311+
const user = userEvent.setup();
312+
await user.pullToRefresh(scrollView);
313+
```
314+
315+
Simulates a user performing the pull-to-refresh gesture on a host `ScrollView` element, invoking the `onRefresh` handler of its `refreshControl` prop.
316+
317+
This function supports only host `ScrollView` elements, passing other element types will result in an error. Note that `FlatList` and `SectionList` are accepted as they render to a host `ScrollView` element.
318+
319+
If the element has no `refreshControl` prop, or its `RefreshControl` has no `onRefresh` handler, the call resolves without doing anything.
320+
297321
## `accessibilityAction()`
298322

299323
> [!NOTE]

src/user-event/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const userEvent = {
2121
paste: (instance: TestInstance, text: string) => setup().paste(instance, text),
2222
scrollTo: (instance: TestInstance, options: ScrollToOptions) =>
2323
setup().scrollTo(instance, options),
24+
pullToRefresh: (instance: TestInstance) => setup().pullToRefresh(instance),
2425
accessibilityAction: (instance: TestInstance, actionName: AccessibilityActionName) =>
2526
setup().accessibilityAction(instance, actionName),
2627
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from 'react';
2+
import { FlatList, RefreshControl, ScrollView, SectionList, Text, View } from 'react-native';
3+
4+
import { render, screen, userEvent } from '../../..';
5+
6+
describe('pullToRefresh()', () => {
7+
it('supports ScrollView', async () => {
8+
const onRefreshMock = jest.fn();
9+
await render(
10+
<ScrollView
11+
testID="view"
12+
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
13+
/>,
14+
);
15+
const user = userEvent.setup();
16+
17+
await user.pullToRefresh(screen.getByTestId('view'));
18+
expect(onRefreshMock).toHaveBeenCalled();
19+
});
20+
21+
it('supports FlatList', async () => {
22+
const onRefreshMock = jest.fn();
23+
await render(
24+
<FlatList
25+
testID="view"
26+
data={['A', 'B', 'C']}
27+
renderItem={({ item }) => <Text>{item}</Text>}
28+
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
29+
/>,
30+
);
31+
const user = userEvent.setup();
32+
33+
await user.pullToRefresh(screen.getByTestId('view'));
34+
expect(onRefreshMock).toHaveBeenCalled();
35+
});
36+
37+
it('supports SectionList', async () => {
38+
const onRefreshMock = jest.fn();
39+
await render(
40+
<SectionList
41+
testID="view"
42+
sections={[
43+
{ title: 'Section 1', data: ['A', 'B', 'C'] },
44+
{ title: 'Section 2', data: ['D', 'E', 'F'] },
45+
]}
46+
renderItem={({ item }) => <Text>{item}</Text>}
47+
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
48+
/>,
49+
);
50+
const user = userEvent.setup();
51+
52+
await user.pullToRefresh(screen.getByTestId('view'));
53+
expect(onRefreshMock).toHaveBeenCalled();
54+
});
55+
56+
it('does not throw when RefreshControl is not set', async () => {
57+
await render(<ScrollView testID="view" />);
58+
const user = userEvent.setup();
59+
60+
await expect(user.pullToRefresh(screen.getByTestId('view'))).resolves.toBeUndefined();
61+
});
62+
63+
it('does not throw when RefreshControl onRefresh is not set', async () => {
64+
await render(
65+
<ScrollView testID="view" refreshControl={<RefreshControl refreshing={false} />} />,
66+
);
67+
const user = userEvent.setup();
68+
69+
await expect(user.pullToRefresh(screen.getByTestId('view'))).resolves.toBeUndefined();
70+
});
71+
72+
it('throws when passed a non-ScrollView element', async () => {
73+
await render(<View testID="view" />);
74+
const user = userEvent.setup();
75+
76+
await expect(user.pullToRefresh(screen.getByTestId('view'))).rejects.toThrow(
77+
/pullToRefresh\(\) works only with host "ScrollView" elements/,
78+
);
79+
});
80+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { TestInstance } from 'test-renderer';
2+
3+
import { act } from '../../act';
4+
import { ErrorWithStack } from '../../helpers/errors';
5+
import { isHostScrollView } from '../../helpers/host-component-names';
6+
import type { UserEventInstance } from '../setup';
7+
8+
export async function pullToRefresh(
9+
this: UserEventInstance,
10+
instance: TestInstance,
11+
): Promise<void> {
12+
if (!isHostScrollView(instance)) {
13+
throw new ErrorWithStack(
14+
`pullToRefresh() works only with host "ScrollView" elements. Passed element has type "${instance.type}".`,
15+
pullToRefresh,
16+
);
17+
}
18+
19+
const refreshControl = instance.props.refreshControl;
20+
if (refreshControl == null || typeof refreshControl.props.onRefresh !== 'function') {
21+
return;
22+
}
23+
24+
// eslint-disable-next-line require-await
25+
await act(async () => {
26+
refreshControl.props.onRefresh();
27+
});
28+
}

src/user-event/setup/setup.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { PressOptions } from '../press';
1111
import { longPress, press } from '../press';
1212
import type { ScrollToOptions } from '../scroll';
1313
import { scrollTo } from '../scroll';
14+
import { pullToRefresh } from '../scroll/pull-to-refresh';
1415
import type { TypeOptions } from '../type';
1516
import { type } from '../type';
1617
import { wait } from '../utils';
@@ -149,13 +150,25 @@ export interface UserEventInstance {
149150
paste: (instance: TestInstance, text: string) => Promise<void>;
150151

151152
/**
152-
* Simlate user scorlling a ScrollView element.
153+
* Simlate user scorlling a given `ScrollView`-like element.
153154
*
154-
* @param instance ScrollView instance
155+
* Supported components: ScrollView, FlatList, SectionList
156+
*
157+
* @param instance ScrollView-like instance
155158
* @returns
156159
*/
157160
scrollTo: (instance: TestInstance, options: ScrollToOptions) => Promise<void>;
158161

162+
/**
163+
* Simulate using pull-to-refresh gesture on a given `ScrollView`-like element.
164+
*
165+
* Supported components: ScrollView, FlatList, SectionList
166+
*
167+
* @param instance ScrollView-like instance
168+
* @returns
169+
*/
170+
pullToRefresh: (instance: TestInstance) => Promise<void>;
171+
159172
/**
160173
* Simulate an assistive technology (e.g. screen reader) triggering an
161174
* accessibility action on a given element.
@@ -185,6 +198,7 @@ function createInstance(config: UserEventConfig): UserEventInstance {
185198
clear: wrapAndBindImpl(instance, clear),
186199
paste: wrapAndBindImpl(instance, paste),
187200
scrollTo: wrapAndBindImpl(instance, scrollTo),
201+
pullToRefresh: wrapAndBindImpl(instance, pullToRefresh),
188202
accessibilityAction: wrapAndBindImpl(instance, accessibilityAction),
189203
};
190204

0 commit comments

Comments
 (0)