diff --git a/AGENTS.md b/AGENTS.md index 0c0e938b6..d233adba0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,9 @@ `@testing-library/react-native` is a TypeScript/Jest library for testing React Native components with user-focused testing patterns. +> [!IMPORTANT] +> Never run git commands that create commits, push, or modify the index/history (`git commit`, `git push`, `git add`, `git rm`, `git reset`, `git rebase`, `git merge`, `git stash`, `git tag`, etc.). Only make working-tree changes and read-only git inspections; the human stages and commits. See [Git, releases, and PR workflow](agents/git-workflow.md). + - Package manager: `yarn` (`yarn@4.11.0`) - Common commands: - `yarn test` diff --git a/CHANGELOG.md b/CHANGELOG.md index baea67f71..6e6a1754c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ with v14. `onLayout` handler with a synthetic layout event. - Added `userEvent.accessibilityAction()` to dispatch a named accessibility action to an element, invoking its `onAccessibilityAction` handler. +- Added `userEvent.pullToRefresh()` to simulate the pull-to-refresh gesture on a host + `ScrollView` element, invoking the `onRefresh` handler of its `refreshControl` prop. ## 14.0.0 diff --git a/agents/git-workflow.md b/agents/git-workflow.md index 6a0d60560..daef86153 100644 --- a/agents/git-workflow.md +++ b/agents/git-workflow.md @@ -1,5 +1,12 @@ # Git, Releases, And PR Workflow +## Agent git restrictions + +- Never run git commands that create commits, push, or modify the index/history. The human owns these actions. +- Forbidden commands include (non-exhaustive): `git commit`, `git push`, `git add`, `git rm`, `git restore --staged`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, `git stash`, `git commit --amend`, and `git tag`. +- Read-only inspection is fine: `git status`, `git log`, `git diff`, `git show`, `git blame`. +- When conflicts or staging are involved, resolve file contents in the working tree only, then hand off to the human to stage and commit. Describe the exact commands you would run instead of running them. + ## Commits and releases - Use Conventional Commits such as `fix:`, `feat:`, and `chore:`. diff --git a/docs/api/user-event.md b/docs/api/user-event.md index 234ec5b5e..5e38ac315 100644 --- a/docs/api/user-event.md +++ b/docs/api/user-event.md @@ -294,6 +294,30 @@ The sequence of events depends on whether the scroll includes an optional moment - `scroll` (multiple events) - `momentumScrollEnd` +## `pullToRefresh()` \ + +> [!NOTE] +> Available since React Native Testing Library 14.1.0. + +```ts +pullToRefresh( + instance: TestInstance, +): Promise +``` + +Example + +```ts +const user = userEvent.setup(); +await user.pullToRefresh(scrollView); +``` + +Simulates a user performing the pull-to-refresh gesture on a host `ScrollView` element, invoking the `onRefresh` handler of its `refreshControl` prop. + +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. + +If the element has no `refreshControl` prop, or its `RefreshControl` has no `onRefresh` handler, the call resolves without doing anything. + ## `accessibilityAction()` > [!NOTE] diff --git a/src/user-event/index.ts b/src/user-event/index.ts index b19451cdf..fad60a88a 100644 --- a/src/user-event/index.ts +++ b/src/user-event/index.ts @@ -21,6 +21,7 @@ export const userEvent = { paste: (instance: TestInstance, text: string) => setup().paste(instance, text), scrollTo: (instance: TestInstance, options: ScrollToOptions) => setup().scrollTo(instance, options), + pullToRefresh: (instance: TestInstance) => setup().pullToRefresh(instance), accessibilityAction: (instance: TestInstance, actionName: AccessibilityActionName) => setup().accessibilityAction(instance, actionName), }; 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..74e432d73 --- /dev/null +++ b/src/user-event/scroll/__tests__/pull-to-refresh.test.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { FlatList, RefreshControl, ScrollView, SectionList, Text, View } from 'react-native'; + +import { render, screen, userEvent } from '../../..'; + +describe('pullToRefresh()', () => { + test('supports ScrollView', async () => { + const onRefreshMock = jest.fn(); + await render( + } + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); + + test('supports FlatList', async () => { + const onRefreshMock = jest.fn(); + await render( + {item}} + refreshControl={} + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); + + test('supports SectionList', async () => { + const onRefreshMock = jest.fn(); + await render( + {item}} + refreshControl={} + />, + ); + const user = userEvent.setup(); + + await user.pullToRefresh(screen.getByTestId('view')); + expect(onRefreshMock).toHaveBeenCalled(); + }); + + test('does not throw when RefreshControl is not set', async () => { + await render(); + const user = userEvent.setup(); + + await expect(user.pullToRefresh(screen.getByTestId('view'))).resolves.toBeUndefined(); + }); + + test('does not throw when RefreshControl onRefresh is not set', async () => { + await render( + } />, + ); + const user = userEvent.setup(); + + await expect(user.pullToRefresh(screen.getByTestId('view'))).resolves.toBeUndefined(); + }); + + test('throws when passed a non-ScrollView element', async () => { + await render(); + const user = userEvent.setup(); + + await expect(user.pullToRefresh(screen.getByTestId('view'))).rejects.toThrow( + /pullToRefresh\(\) works only with host "ScrollView" instances/, + ); + }); +}); diff --git a/src/user-event/scroll/index.ts b/src/user-event/scroll/index.ts index 2eb6ff196..2307641fc 100644 --- a/src/user-event/scroll/index.ts +++ b/src/user-event/scroll/index.ts @@ -1 +1,2 @@ +export { pullToRefresh } from './pull-to-refresh'; export { scrollTo, ScrollToOptions } from './scroll-to'; 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..e22fe2645 --- /dev/null +++ b/src/user-event/scroll/pull-to-refresh.ts @@ -0,0 +1,27 @@ +import type { TestInstance } from '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, + instance: TestInstance, +): Promise { + if (!isHostScrollView(instance)) { + throw new ErrorWithStack( + `pullToRefresh() works only with host "ScrollView" instances. Passed instance has type "${instance.type}".`, + pullToRefresh, + ); + } + + const refreshControl = instance.props.refreshControl; + if (typeof refreshControl?.props?.onRefresh !== 'function') { + return; + } + + await act(() => { + refreshControl.props.onRefresh(); + }); +} diff --git a/src/user-event/setup/setup.ts b/src/user-event/setup/setup.ts index feb75d32d..257d3627a 100644 --- a/src/user-event/setup/setup.ts +++ b/src/user-event/setup/setup.ts @@ -10,7 +10,7 @@ import { paste } from '../paste'; import type { PressOptions } from '../press'; import { longPress, press } from '../press'; import type { ScrollToOptions } from '../scroll'; -import { scrollTo } from '../scroll'; +import { pullToRefresh, scrollTo } from '../scroll'; import type { TypeOptions } from '../type'; import { type } from '../type'; import { wait } from '../utils'; @@ -149,13 +149,25 @@ export interface UserEventInstance { paste: (instance: TestInstance, text: string) => Promise; /** - * Simlate user scorlling a ScrollView element. + * Simulate user scrolling a given `ScrollView`-like element. * - * @param instance ScrollView instance + * Supported components: ScrollView, FlatList, SectionList + * + * @param instance ScrollView-like instance * @returns */ scrollTo: (instance: TestInstance, options: ScrollToOptions) => Promise; + /** + * Simulate using pull-to-refresh gesture on a given `ScrollView`-like element. + * + * Supported components: ScrollView, FlatList, SectionList + * + * @param instance ScrollView-like instance + * @returns + */ + pullToRefresh: (instance: TestInstance) => Promise; + /** * Simulate an assistive technology (e.g. screen reader) triggering an * accessibility action on a given element. @@ -185,6 +197,7 @@ function createInstance(config: UserEventConfig): UserEventInstance { clear: wrapAndBindImpl(instance, clear), paste: wrapAndBindImpl(instance, paste), scrollTo: wrapAndBindImpl(instance, scrollTo), + pullToRefresh: wrapAndBindImpl(instance, pullToRefresh), accessibilityAction: wrapAndBindImpl(instance, accessibilityAction), }; 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 4ecdbe85e..efa0e4dc2 100644 --- a/website/docs/14.x/docs/api/events/user-event.mdx +++ b/website/docs/14.x/docs/api/events/user-event.mdx @@ -295,6 +295,31 @@ The sequence of events depends on whether the scroll includes an optional moment - `scroll` (multiple events) - `momentumScrollEnd` +## `pullToRefresh()` \{#pull-to-refresh} + +:::note +Available since React Native Testing Library 14.1.0. +::: + +```ts +pullToRefresh( + instance: TestInstance, +): Promise +``` + +Example + +```ts +const user = userEvent.setup(); +await user.pullToRefresh(scrollView); +``` + +Simulates a user performing the pull-to-refresh gesture on a host `ScrollView` element, invoking the `onRefresh` handler of its `refreshControl` prop. + +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. + +If the element has no `refreshControl` prop, or its `RefreshControl` has no `onRefresh` handler, the call resolves without doing anything. + ## `accessibilityAction()` :::note