From 22c25bec02131dca87d465552a0e5198de6652c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Sep 2025 10:23:37 +0200 Subject: [PATCH 1/2] feat: User Event `pullToRefresh()` --- src/user-event/index.ts | 1 + src/user-event/pull-to-refresh.ts | 0 .../scroll/__tests__/pull-to-refresh.test.tsx | 55 +++++++++++++++++++ src/user-event/scroll/pull-to-refresh.ts | 28 ++++++++++ src/user-event/setup/setup.ts | 18 +++++- 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/user-event/pull-to-refresh.ts create mode 100644 src/user-event/scroll/__tests__/pull-to-refresh.test.tsx create mode 100644 src/user-event/scroll/pull-to-refresh.ts diff --git a/src/user-event/index.ts b/src/user-event/index.ts index a94f54bec..87c93465f 100644 --- a/src/user-event/index.ts +++ b/src/user-event/index.ts @@ -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), }; diff --git a/src/user-event/pull-to-refresh.ts b/src/user-event/pull-to-refresh.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx b/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx new file mode 100644 index 000000000..8e0623dce --- /dev/null +++ b/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx @@ -0,0 +1,55 @@ +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( + } + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); + + it('supports FlatList', async () => { + const onRefreshMock = jest.fn(); + render( + {item}} + refreshControl={} + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); + + it('supports SectionList', async () => { + const onRefreshMock = jest.fn(); + render( + {item}} + refreshControl={} + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); +}); diff --git a/src/user-event/scroll/pull-to-refresh.ts b/src/user-event/scroll/pull-to-refresh.ts new file mode 100644 index 000000000..3189b32cf --- /dev/null +++ b/src/user-event/scroll/pull-to-refresh.ts @@ -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 { + 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') { + return; + } + + // eslint-disable-next-line require-await + await act(async () => { + refreshControl.props.onRefresh(); + }); +} diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index e6d164e50..419848bce 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -11,6 +11,7 @@ import { scrollTo } from '../scroll'; import type { TypeOptions } from '../type'; import { type } from '../type'; import { wait } from '../utils'; +import { pullToRefresh } from '../scroll/pull-to-refresh'; export interface UserEventSetupOptions { /** @@ -138,12 +139,24 @@ export interface UserEventInstance { paste: (element: ReactTestInstance, text: string) => Promise; /** - * 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; + + /** + * 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; } function createInstance(config: UserEventConfig): UserEventInstance { @@ -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); From a8f0ce429be504b44653689a2795e7d06a994627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Sep 2025 10:26:04 +0200 Subject: [PATCH 2/2] more tests --- .../scroll/__tests__/pull-to-refresh.test.tsx | 14 ++++++++++++++ src/user-event/setup/setup.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx b/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx index 8e0623dce..2d2451b83 100644 --- a/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx +++ b/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx @@ -52,4 +52,18 @@ describe('pullToRefresh()', () => { await user.pullToRefresh(screen.getByTestId('view')); expect(onRefreshMock).toHaveBeenCalled(); }); + + it('does not throw when RefreshControl is not set', async () => { + render(); + 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(} />); + const user = userEvent.setup(); + + await expect(() => user.pullToRefresh(screen.getByTestId('view'))).not.toThrow(); + }); }); diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index 419848bce..f8105cb53 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -8,10 +8,10 @@ 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'; -import { pullToRefresh } from '../scroll/pull-to-refresh'; export interface UserEventSetupOptions { /**