Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/user-event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export const userEvent = {
paste: (element: ReactTestInstance, text: string) => setup().paste(element, text),
scrollTo: (element: ReactTestInstance, options: ScrollToOptions) =>
setup().scrollTo(element, options),
pullToRefresh: (element: ReactTestInstance) => setup().pullToRefresh(element),
};
Empty file.
69 changes: 69 additions & 0 deletions src/user-event/scroll/__tests__/pull-to-refresh.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import { FlatList, RefreshControl, ScrollView, SectionList, Text } from 'react-native';

import { render, screen, userEvent } from '../../..';

describe('pullToRefresh()', () => {
it('supports ScrollView', async () => {
const onRefreshMock = jest.fn();
render(
<ScrollView
testID="view"
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
/>,
);
const user = userEvent.setup();

await user.pullToRefresh(screen.getByTestId('view'));
expect(onRefreshMock).toHaveBeenCalled();
});

it('supports FlatList', async () => {
const onRefreshMock = jest.fn();
render(
<FlatList
testID="view"
data={['A', 'B', 'C']}
renderItem={({ item }) => <Text>{item}</Text>}
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
/>,
);
const user = userEvent.setup();

await user.pullToRefresh(screen.getByTestId('view'));
expect(onRefreshMock).toHaveBeenCalled();
});

it('supports SectionList', async () => {
const onRefreshMock = jest.fn();
render(
<SectionList
testID="view"
sections={[
{ title: 'Section 1', data: ['A', 'B', 'C'] },
{ title: 'Section 2', data: ['D', 'E', 'F'] },
]}
renderItem={({ item }) => <Text>{item}</Text>}
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
/>,
);
const user = userEvent.setup();

await user.pullToRefresh(screen.getByTestId('view'));
expect(onRefreshMock).toHaveBeenCalled();
});

it('does not throw when RefreshControl is not set', async () => {
render(<ScrollView testID="view" />);
const user = userEvent.setup();

await expect(() => user.pullToRefresh(screen.getByTestId('view'))).not.toThrow();
});

it('does not throw when RefreshControl onRefresh is not set', async () => {
render(<ScrollView testID="view" refreshControl={<RefreshControl refreshing={false} />} />);
const user = userEvent.setup();

await expect(() => user.pullToRefresh(screen.getByTestId('view'))).not.toThrow();
});
});
28 changes: 28 additions & 0 deletions src/user-event/scroll/pull-to-refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ReactTestInstance } from 'react-test-renderer';

import act from '../../act';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostScrollView } from '../../helpers/host-component-names';
import type { UserEventInstance } from '../setup';

export async function pullToRefresh(
this: UserEventInstance,
element: ReactTestInstance,
): Promise<void> {
if (!isHostScrollView(element)) {
throw new ErrorWithStack(
`pullToRefresh() works only with host "ScrollView" elements. Passed element has type "${element.type}".`,
pullToRefresh,
);
}

const refreshControl = element.props.refreshControl;
if (refreshControl == null || typeof refreshControl.props.onRefresh !== 'function') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Refresh Control Access Error & Misleading Documentation

Accessing refreshControl.props.onRefresh can lead to a TypeError if refreshControl.props is null or undefined. Separately, the error message for pullToRefresh() is misleading, stating it only works with ScrollView elements when FlatList and SectionList are also supported.

Fix in Cursor Fix in Web

return;
}

// eslint-disable-next-line require-await
await act(async () => {
refreshControl.props.onRefresh();
});
}
18 changes: 16 additions & 2 deletions src/user-event/setup/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { PressOptions } from '../press';
import { longPress, press } from '../press';
import type { ScrollToOptions } from '../scroll';
import { scrollTo } from '../scroll';
import { pullToRefresh } from '../scroll/pull-to-refresh';
import type { TypeOptions } from '../type';
import { type } from '../type';
import { wait } from '../utils';
Expand Down Expand Up @@ -138,12 +139,24 @@ export interface UserEventInstance {
paste: (element: ReactTestInstance, text: string) => Promise<void>;

/**
* Simlate user scorlling a ScrollView element.
* Simlate user scorlling a given `ScrollView`-like element.
*
* @param element ScrollView element
* Supported components: ScrollView, FlatList, SectionList
*
* @param element ScrollView-like element
* @returns
*/
scrollTo: (element: ReactTestInstance, options: ScrollToOptions) => Promise<void>;

/**
* Simulate using pull-to-refresh gesture on a given `ScrollView`-like element.
*
* Supported components: ScrollView, FlatList, SectionList
*
* @param element ScrollView-like element
* @returns
*/
pullToRefresh: (element: ReactTestInstance) => Promise<void>;
}

function createInstance(config: UserEventConfig): UserEventInstance {
Expand All @@ -159,6 +172,7 @@ function createInstance(config: UserEventConfig): UserEventInstance {
clear: wrapAndBindImpl(instance, clear),
paste: wrapAndBindImpl(instance, paste),
scrollTo: wrapAndBindImpl(instance, scrollTo),
pullToRefresh: wrapAndBindImpl(instance, pullToRefresh),
};

Object.assign(instance, api);
Expand Down