Skip to content
Merged
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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions agents/git-workflow.md
Original file line number Diff line number Diff line change
@@ -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:`.
Expand Down
24 changes: 24 additions & 0 deletions docs/api/user-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
```

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]
Expand Down
1 change: 1 addition & 0 deletions src/user-event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
80 changes: 80 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,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(
<ScrollView
testID="view"
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefreshMock} />}
/>,
);
const user = userEvent.setup();

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

test('supports FlatList', async () => {
const onRefreshMock = jest.fn();
await 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();
});

test('supports SectionList', async () => {
const onRefreshMock = jest.fn();
await 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();
});

test('does not throw when RefreshControl is not set', async () => {
await render(<ScrollView testID="view" />);
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(
<ScrollView testID="view" refreshControl={<RefreshControl refreshing={false} />} />,
);
const user = userEvent.setup();

await expect(user.pullToRefresh(screen.getByTestId('view'))).resolves.toBeUndefined();
});

test('throws when passed a non-ScrollView element', async () => {
await render(<View testID="view" />);
const user = userEvent.setup();

await expect(user.pullToRefresh(screen.getByTestId('view'))).rejects.toThrow(
/pullToRefresh\(\) works only with host "ScrollView" instances/,
);
});
});
1 change: 1 addition & 0 deletions src/user-event/scroll/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { pullToRefresh } from './pull-to-refresh';
export { scrollTo, ScrollToOptions } from './scroll-to';
27 changes: 27 additions & 0 deletions src/user-event/scroll/pull-to-refresh.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
});
}
19 changes: 16 additions & 3 deletions src/user-event/setup/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -149,13 +149,25 @@ export interface UserEventInstance {
paste: (instance: TestInstance, text: string) => Promise<void>;

/**
* 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<void>;

/**
* 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<void>;

/**
* Simulate an assistive technology (e.g. screen reader) triggering an
* accessibility action on a given element.
Expand Down Expand Up @@ -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),
};

Expand Down
25 changes: 25 additions & 0 deletions website/docs/14.x/docs/api/events/user-event.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
```

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
Expand Down