feat: add screen API (#994)

* feature: screen

* feature: update docs

* refactor: add flow type

* docs: more

* docs: final tweaks

* test rerender

* code review changes

* alias RenderResult to RenderAPI

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
This commit is contained in:
Maciej Jastrzebski
2022-06-24 13:17:06 +02:00
committed by GitHub
parent 1e67026d95
commit 5d681790bf
14 changed files with 358 additions and 139 deletions
+4 -6
View File
@@ -102,22 +102,20 @@ flow-typed install react-test-renderer
## Example
```jsx
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { QuestionsBoard } from '../QuestionsBoard';
test('form submits two answers', () => {
const allQuestions = ['q1', 'q2'];
const mockFn = jest.fn();
const { getAllByLabelText, getByText } = render(
<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />
);
render(<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />);
const answerInputs = getAllByLabelText('answer input');
const answerInputs = screen.getAllByLabelText('answer input');
fireEvent.changeText(answerInputs[0], 'a1');
fireEvent.changeText(answerInputs[1], 'a2');
fireEvent.press(getByText('Submit'));
fireEvent.press(screen.getByText('Submit'));
expect(mockFn).toBeCalledWith({
'1': { q: 'q1', a: 'a1' },
+67
View File
@@ -0,0 +1,67 @@
import * as React from 'react';
import { View, Text } from 'react-native';
import { render, screen } from '..';
test('screen has the same queries as render result', () => {
const result = render(<Text>Mt. Everest</Text>);
expect(screen).toBe(result);
expect(screen.getByText('Mt. Everest')).toBeTruthy();
expect(screen.queryByText('Mt. Everest')).toBeTruthy();
expect(screen.getAllByText('Mt. Everest')).toHaveLength(1);
expect(screen.queryAllByText('Mt. Everest')).toHaveLength(1);
});
test('screen holds last render result', () => {
render(<Text>Mt. Everest</Text>);
render(<Text>Mt. Blanc</Text>);
const finalResult = render(<Text>Śnieżka</Text>);
expect(screen).toBe(finalResult);
expect(screen.getByText('Śnieżka')).toBeTruthy();
expect(screen.queryByText('Mt. Everest')).toBeFalsy();
expect(screen.queryByText('Mt. Blanc')).toBeFalsy();
});
test('screen works with updating rerender', () => {
const result = render(<Text>Mt. Everest</Text>);
expect(screen).toBe(result);
screen.rerender(<Text>Śnieżka</Text>);
expect(screen).toBe(result);
expect(screen.getByText('Śnieżka')).toBeTruthy();
});
test('screen works with nested re-mounting rerender', () => {
const result = render(
<View>
<Text>Mt. Everest</Text>
</View>
);
expect(screen).toBe(result);
screen.rerender(
<View>
<View>
<Text>Śnieżka</Text>
</View>
</View>
);
expect(screen).toBe(result);
expect(screen.getByText('Śnieżka')).toBeTruthy();
});
test('screen throws without render', () => {
expect(() => screen.container).toThrowError(
'`render` method has not been called'
);
expect(() => screen.debug()).toThrowError(
'`render` method has not been called'
);
expect(() => screen.debug.shallow()).toThrowError(
'`render` method has not been called'
);
expect(() => screen.getByText('Mt. Everest')).toThrowError(
'`render` method has not been called'
);
});
+2
View File
@@ -1,9 +1,11 @@
import * as React from 'react';
import { clearRenderResult } from './screen';
type CleanUpFunction = (nextElement?: React.ReactElement<any>) => void;
let cleanupQueue = new Set<CleanUpFunction>();
export default function cleanup() {
clearRenderResult();
cleanupQueue.forEach((fn) => fn());
cleanupQueue.clear();
}
+6 -2
View File
@@ -1,12 +1,16 @@
import act from './act';
import cleanup from './cleanup';
import fireEvent from './fireEvent';
import render from './render';
import render, { RenderResult } from './render';
import waitFor from './waitFor';
import waitForElementToBeRemoved from './waitForElementToBeRemoved';
import { within, getQueriesForElement } from './within';
import { getDefaultNormalizer } from './matches';
import { renderHook } from './renderHook';
import { screen } from './screen';
export type { RenderResult };
export type RenderAPI = RenderResult;
export { act };
export { cleanup };
@@ -17,4 +21,4 @@ export { waitForElementToBeRemoved };
export { within, getQueriesForElement };
export { getDefaultNormalizer };
export { renderHook };
export type RenderAPI = ReturnType<typeof render>;
export { screen };
+7 -1
View File
@@ -6,6 +6,7 @@ import { addToCleanupQueue } from './cleanup';
import debugShallow from './helpers/debugShallow';
import debugDeep from './helpers/debugDeep';
import { getQueriesForElement } from './within';
import { setRenderResult } from './screen';
type Options = {
wrapper?: React.ComponentType<any>;
@@ -15,6 +16,8 @@ type TestRendererOptions = {
createNodeMock: (element: React.ReactElement) => any;
};
export type RenderResult = ReturnType<typeof render>;
/**
* Renders test component deeply using react-test-renderer and exposes helpers
* to assert on the output.
@@ -40,7 +43,7 @@ export default function render<T>(
addToCleanupQueue(unmount);
return {
const result = {
...getQueriesForElement(instance),
update,
unmount,
@@ -49,6 +52,9 @@ export default function render<T>(
toJSON: renderer.toJSON,
debug: debug(instance, renderer),
};
setRenderResult(result);
return result;
}
function renderWithAct(
+108
View File
@@ -0,0 +1,108 @@
import { ReactTestInstance } from 'react-test-renderer';
import { RenderResult } from './render';
const SCREEN_ERROR = '`render` method has not been called';
const notImplemented = () => {
throw new Error(SCREEN_ERROR);
};
const notImplementedDebug = () => {
throw new Error(SCREEN_ERROR);
};
notImplementedDebug.shallow = notImplemented;
const defaultScreen: RenderResult = {
get container(): ReactTestInstance {
throw new Error(SCREEN_ERROR);
},
debug: notImplementedDebug,
update: notImplemented,
unmount: notImplemented,
rerender: notImplemented,
toJSON: notImplemented,
getByLabelText: notImplemented,
getAllByLabelText: notImplemented,
queryByLabelText: notImplemented,
queryAllByLabelText: notImplemented,
findByLabelText: notImplemented,
findAllByLabelText: notImplemented,
getByA11yHint: notImplemented,
getByHintText: notImplemented,
getAllByA11yHint: notImplemented,
getAllByHintText: notImplemented,
queryByA11yHint: notImplemented,
queryByHintText: notImplemented,
queryAllByA11yHint: notImplemented,
queryAllByHintText: notImplemented,
findByA11yHint: notImplemented,
findByHintText: notImplemented,
findAllByA11yHint: notImplemented,
findAllByHintText: notImplemented,
getByRole: notImplemented,
getAllByRole: notImplemented,
queryByRole: notImplemented,
queryAllByRole: notImplemented,
findByRole: notImplemented,
findAllByRole: notImplemented,
getByA11yStates: notImplemented,
getAllByA11yStates: notImplemented,
queryByA11yStates: notImplemented,
queryAllByA11yStates: notImplemented,
findByA11yStates: notImplemented,
findAllByA11yStates: notImplemented,
getByA11yState: notImplemented,
getAllByA11yState: notImplemented,
queryByA11yState: notImplemented,
queryAllByA11yState: notImplemented,
findByA11yState: notImplemented,
findAllByA11yState: notImplemented,
getByA11yValue: notImplemented,
getAllByA11yValue: notImplemented,
queryByA11yValue: notImplemented,
queryAllByA11yValue: notImplemented,
findByA11yValue: notImplemented,
findAllByA11yValue: notImplemented,
UNSAFE_getByProps: notImplemented,
UNSAFE_getAllByProps: notImplemented,
UNSAFE_queryByProps: notImplemented,
UNSAFE_queryAllByProps: notImplemented,
UNSAFE_getByType: notImplemented,
UNSAFE_getAllByType: notImplemented,
UNSAFE_queryByType: notImplemented,
UNSAFE_queryAllByType: notImplemented,
getByPlaceholderText: notImplemented,
getAllByPlaceholderText: notImplemented,
queryByPlaceholderText: notImplemented,
queryAllByPlaceholderText: notImplemented,
findByPlaceholderText: notImplemented,
findAllByPlaceholderText: notImplemented,
getByDisplayValue: notImplemented,
getAllByDisplayValue: notImplemented,
queryByDisplayValue: notImplemented,
queryAllByDisplayValue: notImplemented,
findByDisplayValue: notImplemented,
findAllByDisplayValue: notImplemented,
getByTestId: notImplemented,
getAllByTestId: notImplemented,
queryByTestId: notImplemented,
queryAllByTestId: notImplemented,
findByTestId: notImplemented,
findAllByTestId: notImplemented,
getByText: notImplemented,
getAllByText: notImplemented,
queryByText: notImplemented,
queryAllByText: notImplemented,
findByText: notImplemented,
findAllByText: notImplemented,
};
export let screen: RenderResult = defaultScreen;
export function setRenderResult(output: RenderResult) {
screen = output;
}
export function clearRenderResult() {
screen = defaultScreen;
}
+2
View File
@@ -363,6 +363,8 @@ declare module '@testing-library/react-native' {
options?: RenderOptions
) => RenderAPI;
declare export var screen: RenderAPI;
declare export var cleanup: () => void;
declare export var fireEvent: FireEventAPI;
+64 -42
View File
@@ -6,30 +6,41 @@ title: API
### Table of contents:
- [`render`](#render)
- [`...queries`](#queries)
- [Example](#example)
- [`update`](#update)
- [`unmount`](#unmount)
- [`debug`](#debug)
- [`debug.shallow`](#debugshallow)
- [`toJSON`](#tojson)
- [`container`](#container)
- [`screen`](#screen)
- [`cleanup`](#cleanup)
- [`fireEvent`](#fireevent)
- [`fireEvent[eventName]`](#fireeventeventname)
- [`fireEvent.press`](#fireeventpress)
- [`fireEvent.changeText`](#fireeventchangetext)
- [`fireEvent.scroll`](#fireeventscroll)
- [On a `ScrollView`](#on-a-scrollview)
- [On a `FlatList`](#on-a-flatlist)
- [`waitFor`](#waitfor)
- [`waitForElementToBeRemoved`](#waitforelementtoberemoved)
- [`within, getQueriesForElement`](#within-getqueriesforelement)
- [`within`, `getQueriesForElement`](#within-getqueriesforelement)
- [`query` APIs](#query-apis)
- [`queryAll` APIs](#queryall-apis)
- [`act`](#act)
- [`renderHook`](#renderhook)
- [`callback`](#callback)
- [`options`](#options-optional)
- [`options` (Optional)](#options-optional)
- [`initialProps`](#initialprops)
- [`wrapper`](#wrapper)
- [`RenderHookResult` object](#renderhookresult-object)
- [`result`](#result)
- [`rerender`](#rerender)
- [`unmount`](#unmount-1)
- [Examples](#examples)
- [With `initialProps`](#with-initialprops)
- [With `wrapper`](#with-wrapper)
This page gathers public API of React Native Testing Library along with usage examples.
@@ -58,8 +69,8 @@ import { render } from '@testing-library/react-native';
import { QuestionsBoard } from '../QuestionsBoard';
test('should verify two questions', () => {
const { queryAllByRole } = render(<QuestionsBoard {...props} />);
const allQuestions = queryAllByRole('header');
render(<QuestionsBoard {...props} />);
const allQuestions = screen.queryAllByRole('header');
expect(allQuestions).toHaveLength(2);
});
@@ -67,7 +78,13 @@ test('should verify two questions', () => {
> When using React context providers, like Redux Provider, you'll likely want to wrap rendered component with them. In such cases it's convenient to create your custom `render` method. [Follow this great guide on how to set this up](https://testing-library.com/docs/react-testing-library/setup#custom-render).
The `render` method returns a `RenderResult` object that has a few properties:
The `render` method returns a `RenderResult` object having properties described below.
:::info
Latest `render` result is kept in [`screen`](#screen) variable that can be imported from `@testing-library/react-native` package.
Using `screen` instead of destructuring `render` result is recommended approach. See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details.
:::
### `...queries`
@@ -117,9 +134,9 @@ debug(message?: string): void
Pretty prints deeply rendered component passed to `render` with optional message on top.
```jsx
const { debug } = render(<Component />);
render(<Component />);
debug('optional message');
screen.debug('optional message');
```
logs optional message and colored JSX:
@@ -154,13 +171,23 @@ container: ReactTestInstance;
A reference to the rendered root element.
## `screen`
```ts
let screen: RenderResult;
```
Hold the value of latest render call for easier access to query and other functions returned by [`render`](#render).
Its value is automatically cleared after each test by calling [`cleanup`](#cleanup). If no `render` call has been made in a given test then it holds a special object that implements `RenderResult` but throws a helpful error on each property and method access.
## `cleanup`
```ts
const cleanup: () => void;
```
Unmounts React trees that were mounted with `render`.
Unmounts React trees that were mounted with `render` and clears `screen` variable that holds latest `render` output.
:::info
Please note that this is done automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.
@@ -195,8 +222,6 @@ describe('when logged in', () => {
Failing to call `cleanup` when you've called `render` could result in a memory leak and tests which are not "idempotent" (which can lead to difficult to debug errors in your tests).
The alternative to `cleanup` is balancing every `render` with an `unmount` method call.
## `fireEvent`
```ts
@@ -208,17 +233,17 @@ Fires native-like event with data.
Invokes a given event handler (whether native or custom) on the element, bubbling to the root of the rendered tree.
```jsx
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
test('fire changeText event', () => {
const onEventMock = jest.fn();
const { getByPlaceholderText } = render(
render(
// MyComponent renders TextInput which has a placeholder 'Enter details'
// and with `onChangeText` bound to handleChangeText
<MyComponent handleChangeText={onEventMock} />
);
fireEvent(getByPlaceholderText('change'), 'onChangeText', 'ab');
fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab');
expect(onEventMock).toHaveBeenCalledWith('ab');
});
```
@@ -235,14 +260,14 @@ import { fireEvent, render } from '@testing-library/react-native';
const onBlurMock = jest.fn();
const { getByPlaceholderText } = render(
render(
<View>
<TextInput placeholder="my placeholder" onBlur={onBlurMock} />
</View>
);
// you can omit the `on` prefix
fireEvent(getByPlaceholderText('my placeholder'), 'blur');
fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur');
```
## `fireEvent[eventName]`
@@ -263,7 +288,7 @@ Invokes `press` event handler on the element or parent element in the tree.
```jsx
import { View, Text, TouchableOpacity } from 'react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
const onPressMock = jest.fn();
const eventData = {
@@ -273,7 +298,7 @@ const eventData = {
},
};
const { getByText } = render(
render(
<View>
<TouchableOpacity onPress={onPressMock}>
<Text>Press me</Text>
@@ -281,7 +306,7 @@ const { getByText } = render(
</View>
);
fireEvent.press(getByText('Press me'), eventData);
fireEvent.press(screen.getByText('Press me'), eventData);
expect(onPressMock).toHaveBeenCalledWith(eventData);
```
@@ -295,18 +320,18 @@ Invokes `changeText` event handler on the element or parent element in the tree.
```jsx
import { View, TextInput } from 'react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
const onChangeTextMock = jest.fn();
const CHANGE_TEXT = 'content';
const { getByPlaceholderText } = render(
render(
<View>
<TextInput placeholder="Enter data" onChangeText={onChangeTextMock} />
</View>
);
fireEvent.changeText(getByPlaceholderText('Enter data'), CHANGE_TEXT);
fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT);
```
### `fireEvent.scroll`
@@ -321,7 +346,7 @@ Invokes `scroll` event handler on the element or parent element in the tree.
```jsx
import { ScrollView, Text } from 'react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
const onScrollMock = jest.fn();
const eventData = {
@@ -332,23 +357,23 @@ const eventData = {
},
};
const { getByText } = render(
render(
<ScrollView onScroll={onScrollMock}>
<Text>XD</Text>
</ScrollView>
);
fireEvent.scroll(getByText('scroll-view'), eventData);
fireEvent.scroll(screen.getByText('scroll-view'), eventData);
```
#### On a `FlatList`
```jsx
import { FlatList, View } from 'react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
const onEndReached = jest.fn();
const { getByTestId } = render(
render(
<FlatList
data={Array.from({ length: 10 }, (_, key) => ({ key: `${key}` }))}
renderItem={() => <View style={{ height: 500, width: 100 }} />}
@@ -375,7 +400,7 @@ const eventData = {
},
};
fireEvent.scroll(getByTestId('flat-list'), eventData);
fireEvent.scroll(screen.getByTestId('flat-list'), eventData);
expect(onEndReached).toHaveBeenCalled();
```
@@ -399,12 +424,12 @@ function waitFor<T>(
Waits for non-deterministic periods of time until your element appears or times out. `waitFor` periodically calls `expectation` every `interval` milliseconds to determine whether the element appeared or not.
```jsx
import { render, waitFor } from '@testing-library/react-native';
import { render, screen, waitFor } from '@testing-library/react-native';
test('waiting for an Banana to be ready', async () => {
const { getByText } = render(<Banana />);
render(<Banana />);
await waitFor(() => getByText('Banana ready'));
await waitFor(() => screen.getByText('Banana ready'));
});
```
@@ -428,15 +453,12 @@ function waitForElementToBeRemoved<T>(
Waits for non-deterministic periods of time until queried element is removed or times out. `waitForElementToBeRemoved` periodically calls `expectation` every `interval` milliseconds to determine whether the element has been removed or not.
```jsx
import {
render,
waitForElementToBeRemoved,
} from '@testing-library/react-native';
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
test('waiting for an Banana to be removed', async () => {
const { getByText } = render(<Banana />);
render(<Banana />);
await waitForElementToBeRemoved(() => getByText('Banana ready'));
await waitForElementToBeRemoved(() => screen.getByText('Banana ready'));
});
```
@@ -466,7 +488,7 @@ Please note that additional `render` specific operations like `update`, `unmount
:::
```jsx
const detailsScreen = within(getByA11yHint('Details Screen'));
const detailsScreen = within(screen.getByA11yHint('Details Screen'));
expect(detailsScreen.getByText('Some Text')).toBeTruthy();
expect(detailsScreen.getByDisplayValue('Some Value')).toBeTruthy();
expect(detailsScreen.queryByLabelText('Some Label')).toBeTruthy();
@@ -483,10 +505,10 @@ Use cases for scoped queries include:
Each of the get APIs listed in the render section above have a complimentary query API. The get APIs will throw errors if a proper node cannot be found. This is normally the desired effect. However, if you want to make an assertion that an element is not present in the hierarchy, then you can use the query API instead:
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { queryByText } = render(<Form />);
const submitButton = queryByText('submit');
render(<Form />);
const submitButton = screen.queryByText('submit');
expect(submitButton).toBeNull(); // it doesn't exist
```
@@ -497,8 +519,8 @@ Each of the query APIs have a corresponding queryAll version that always returns
```jsx
import { render } from '@testing-library/react-native';
const { queryAllByText } = render(<Forms />);
const submitButtons = queryAllByText('submit');
render(<Forms />);
const submitButtons = screen.queryAllByText('submit');
expect(submitButtons).toHaveLength(3); // expect 3 elements
```
@@ -6,8 +6,6 @@ title: ESLint Plugin Testing Library Compatibility
Most of the rules of the [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library) are compatible with this library except the followings:
- [prefer-screen-queries](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md): there is no screen object so this rule shouldn't be used
- [prefer-user-event](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-user-event.md): userEvent requires a dom environement so it is not compatible with this library
Also, some rules have become useless, unless maybe you're using an old version of the library:
+12
View File
@@ -31,3 +31,15 @@ The the negative side:
For instance, [react-native's ScrollView](https://reactnative.dev/docs/scrollview) has several props that depend on native calls. While you can trigger `onScroll` call with `fireEvent.scroll`, `onMomentumScrollBegin` is called from the native side and will therefore not be called.
</details>
<details>
<summary>Should I use/migrate to `screen` queries?</summary>
<br />
There is no need to migrate existing test code to use `screen`-bases queries. You can still use
queries and other functions returned by `render`. In fact `screen` hold just that value, the latest `render` result.
For newer code you can either use `screen` or `render` result destructuring. However, there are some good reasons to use `screen`, which are described in [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) by Kent C. Dodds.
</details>
+4 -6
View File
@@ -75,22 +75,20 @@ flow-typed install react-test-renderer
## Example
```jsx
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { QuestionsBoard } from '../QuestionsBoard';
test('form submits two answers', () => {
const allQuestions = ['q1', 'q2'];
const mockFn = jest.fn();
const { getAllByLabelText, getByText } = render(
<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />
);
render(<QuestionsBoard questions={allQuestions} onSubmit={mockFn} />);
const answerInputs = getAllByLabelText('answer input');
const answerInputs = screen.getAllByLabelText('answer input');
fireEvent.changeText(answerInputs[0], 'a1');
fireEvent.changeText(answerInputs[1], 'a2');
fireEvent.press(getByText('Submit'));
fireEvent.press(screen.getByText('Submit'));
expect(mockFn).toBeCalledWith({
'1': { q: 'q1', a: 'a1' },
+56 -54
View File
@@ -6,14 +6,14 @@ title: Queries
### Table of contents:
- [Variants](#variants)
- [`getBy`](#getby)
- [`getAllBy`](#getallby)
- [`queryBy`](#queryby)
- [`queryAllBy`](#queryallby)
- [`findBy`](#findby)
- [`findAllBy`](#findallby)
- [getBy](#getby)
- [getAllBy](#getallby)
- [queryBy](#queryby)
- [queryAllBy](#queryallby)
- [findBy](#findby)
- [findAllBy](#findallby)
- [Queries](#queries)
- [`options`](#options)
- [Options](#options)
- [`ByText`](#bytext)
- [`ByPlaceholderText`](#byplaceholdertext)
- [`ByDisplayValue`](#bydisplayvalue)
@@ -24,11 +24,14 @@ title: Queries
- [`ByRole`](#byrole)
- [`ByA11yState`, `ByAccessibilityState`](#bya11ystate-byaccessibilitystate)
- [`ByA11Value`, `ByAccessibilityValue`](#bya11value-byaccessibilityvalue)
- [`TextMatch`](#textmatch)
- [TextMatch](#textmatch)
- [Examples](#examples)
- [Precision](#precision)
- [Normalization](#normalization)
- [Normalization Examples](#normalization-examples)
- [Unit testing helpers](#unit-testing-helpers)
- [`UNSAFE_ByType`](#unsafe_bytype)
- [`UNSAFE_ByProps`](#unsafe_byprops)
## Variants
@@ -93,10 +96,10 @@ Returns a `ReactTestInstance` with matching text – may be a string or regular
This method will join `<Text>` siblings to find matches, similarly to [how React Native handles these components](https://reactnative.dev/docs/text#containers). This will allow for querying for strings that will be visually rendered together, but may be semantically separate React components.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByText } = render(<MyComponent />);
const element = getByText('banana');
render(<MyComponent />);
const element = screen.getByText('banana');
```
### `ByPlaceholderText`
@@ -106,10 +109,10 @@ const element = getByText('banana');
Returns a `ReactTestInstance` for a `TextInput` with a matching placeholder – may be a string or regular expression.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByPlaceholderText } = render(<MyComponent />);
const element = getByPlaceholderText('username');
render(<MyComponent />);
const element = screen.getByPlaceholderText('username');
```
### `ByDisplayValue`
@@ -119,10 +122,10 @@ const element = getByPlaceholderText('username');
Returns a `ReactTestInstance` for a `TextInput` with a matching display value – may be a string or regular expression.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByDisplayValue } = render(<MyComponent />);
const element = getByDisplayValue('username');
render(<MyComponent />);
const element = screen.getByDisplayValue('username');
```
### `ByTestId`
@@ -132,10 +135,10 @@ const element = getByDisplayValue('username');
Returns a `ReactTestInstance` with matching `testID` prop. `testID` – may be a string or a regular expression.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByTestId } = render(<MyComponent />);
const element = getByTestId('unique-id');
render(<MyComponent />);
const element = screen.getByTestId('unique-id');
```
:::info
@@ -149,10 +152,10 @@ In the spirit of [the guiding principles](https://testing-library.com/docs/guidi
Returns a `ReactTestInstance` with matching `accessibilityLabel` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByLabelText } = render(<MyComponent />);
const element = getByLabelText('my-label');
render(<MyComponent />);
const element = screen.getByLabelText('my-label');
```
### `ByHintText`, `ByA11yHint`, `ByAccessibilityHint`
@@ -164,10 +167,10 @@ const element = getByLabelText('my-label');
Returns a `ReactTestInstance` with matching `accessibilityHint` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByHintText } = render(<MyComponent />);
const element = getByHintText('Plays a song');
render(<MyComponent />);
const element = screen.getByHintText('Plays a song');
```
:::info
@@ -182,11 +185,11 @@ Please consult [Apple guidelines on how `accessibilityHint` should be used](http
Returns a `ReactTestInstance` with matching `accessibilityStates` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByA11yStates } = render(<MyComponent />);
const element = getByA11yStates(['checked']);
const element2 = getByA11yStates('checked');
render(<MyComponent />);
const element = screen.getByA11yStates(['checked']);
const element2 = screen.getByA11yStates('checked');
```
### `ByRole`
@@ -196,10 +199,10 @@ const element2 = getByA11yStates('checked');
Returns a `ReactTestInstance` with matching `accessibilityRole` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByRole } = render(<MyComponent />);
const element = getByRole('button');
render(<MyComponent />);
const element = screen.getByRole('button');
```
### `ByA11yState`, `ByAccessibilityState`
@@ -210,10 +213,10 @@ const element = getByRole('button');
Returns a `ReactTestInstance` with matching `accessibilityState` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByA11yState } = render(<Component />);
const element = getByA11yState({ disabled: true });
render(<Component />);
const element = screen.getByA11yState({ disabled: true });
```
### `ByA11Value`, `ByAccessibilityValue`
@@ -224,10 +227,10 @@ const element = getByA11yState({ disabled: true });
Returns a `ReactTestInstance` with matching `accessibilityValue` prop.
```jsx
import { render } from '@testing-library/react-native';
import { render, screen } from '@testing-library/react-native';
const { getByA11yValue } = render(<Component />);
const element = getByA11yValue({ min: 40 });
render(<Component />);
const element = screen.getByA11yValue({ min: 40 });
```
## TextMatch
@@ -248,34 +251,34 @@ type TextMatchOptions = {
Given the following render:
```jsx
const { getByText } = render(<Text>Hello World</Text>);
render(<Text>Hello World</Text>);
```
Will **find a match**:
```js
// Matching a string:
getByText('Hello World'); // full string match
getByText('llo Worl', { exact: false }); // substring match
getByText('hello world', { exact: false }); // ignore case-sensitivity
screen.getByText('Hello World'); // full string match
screen.getByText('llo Worl', { exact: false }); // substring match
screen.getByText('hello world', { exact: false }); // ignore case-sensitivity
// Matching a regex:
getByText(/World/); // substring match
getByText(/world/i); // substring match, ignore case
getByText(/^hello world$/i); // full string match, ignore case-sensitivity
getByText(/Hello W?oRlD/i); // advanced regex
screen.getByText(/World/); // substring match
screen.getByText(/world/i); // substring match, ignore case
screen.getByText(/^hello world$/i); // full string match, ignore case-sensitivity
screen.getByText(/Hello W?oRlD/i); // advanced regex
```
Will **NOT find a match**
```js
// substring does not match
getByText('llo Worl');
screen.getByText('llo Worl');
// full string does not match
getByText('Goodbye World');
screen.getByText('Goodbye World');
// case-sensitive regex with different case
getByText(/hello world/);
screen.getByText(/hello world/);
```
### Precision
@@ -309,7 +312,7 @@ Specifying a value for `normalizer` replaces the built-in normalization, but you
To perform a match against text without trimming:
```typescript
getByText(node, 'text', {
screen.getByText(node, 'text', {
normalizer: getDefaultNormalizer({ trim: false }),
});
```
@@ -317,9 +320,8 @@ getByText(node, 'text', {
To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior:
```typescript
getByText(node, 'text', {
normalizer: (str) =>
getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''),
screen.getByText(node, 'text', {
normalizer: (str) => getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''),
});
```
+15 -15
View File
@@ -158,7 +158,7 @@ Let's add a [`AppNavigator.test.js`](https://github.com/callstack/react-native-t
```jsx
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import AppNavigator from '../AppNavigator';
@@ -177,10 +177,10 @@ describe('Testing react navigation', () => {
</NavigationContainer>
);
const { findByText, findAllByText } = render(component);
render(component);
const header = await findByText('List of numbers from 1 to 20');
const items = await findAllByText(/Item number/);
const header = await screen.findByText('List of numbers from 1 to 20');
const items = await screen.findAllByText(/Item number/);
expect(header).toBeTruthy();
expect(items.length).toBe(10);
@@ -193,12 +193,12 @@ describe('Testing react navigation', () => {
</NavigationContainer>
);
const { findByText } = render(component);
const toClick = await findByText('Item number 5');
render(component);
const toClick = await screen.findByText('Item number 5');
fireEvent(toClick, 'press');
const newHeader = await findByText('Showing details for 5');
const newBody = await findByText('the number you have chosen is 5');
const newHeader = await screen.findByText('Showing details for 5');
const newBody = await screen.findByText('the number you have chosen is 5');
expect(newHeader).toBeTruthy();
expect(newBody).toBeTruthy();
@@ -314,7 +314,7 @@ Let's add a [`DrawerAppNavigator.test.js`](https://github.com/callstack/react-na
```jsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { render, fireEvent } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import DrawerAppNavigator from '../DrawerAppNavigator';
@@ -326,8 +326,8 @@ describe('Testing react navigation', () => {
</NavigationContainer>
);
const { findByText, findAllByText } = render(component);
const button = await findByText('Go to notifications');
render(component);
const button = await screen.findByText('Go to notifications');
expect(button).toBeTruthy();
});
@@ -339,14 +339,14 @@ describe('Testing react navigation', () => {
</NavigationContainer>
);
const { queryByText, findByText } = render(component);
const oldScreen = queryByText('Welcome!');
const button = await findByText('Go to notifications');
render(component);
const oldScreen = screen.queryByText('Welcome!');
const button = await screen.findByText('Go to notifications');
expect(oldScreen).toBeTruthy();
fireEvent(button, 'press');
const newScreen = await findByText('This is the notifications screen');
const newScreen = await screen.findByText('This is the notifications screen');
expect(newScreen).toBeTruthy();
});
+11 -11
View File
@@ -18,7 +18,7 @@ For `./components/AddTodo.test.js`
```jsx
import * as React from 'react';
import { Provider } from 'react-redux';
import { cleanup, fireEvent, render } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import configureStore from '../store';
import AddTodo from './AddTodo';
@@ -32,16 +32,16 @@ describe('AddTodo component test', () => {
</Provider>
);
const { getByPlaceholderText, getByText } = render(component);
render(component);
// There is a TextInput.
// https://github.com/callstack/react-native-testing-library/blob/ae3d4af370487e1e8fedd8219f77225690aefc59/examples/redux/components/AddTodo.js#L24
const input = getByPlaceholderText(/repository/i);
const input = screen.getByPlaceholderText(/repository/i);
expect(input).toBeTruthy();
const textToEnter = 'This is a random element';
fireEvent.changeText(input, textToEnter);
fireEvent.press(getByText('Submit form'));
fireEvent.press(screen.getByText('Submit form'));
const todosState = store.getState().todos;
@@ -65,7 +65,7 @@ For `./components/TodoList.test.js`
```jsx
import * as React from 'react';
import { Provider } from 'react-redux';
import { fireEvent, render } from '@testing-library/react-native';
import { render, screen, fireEvent } from '@testing-library/react-native';
import configureStore from '../store';
import TodoList from './TodoList';
@@ -87,8 +87,8 @@ describe('TodoList component test', () => {
</Provider>
);
const { getAllByText } = render(component);
const todoElems = getAllByText(/something/i);
render(component);
const todoElems = screen.getAllByText(/something/i);
expect(todoElems.length).toEqual(4);
});
@@ -108,16 +108,16 @@ describe('TodoList component test', () => {
</Provider>
);
const { getAllByText } = render(component);
const todoElems = getAllByText(/something/i);
render(component);
const todoElems = screen.getAllByText(/something/i);
expect(todoElems.length).toBe(2);
const buttons = getAllByText('Delete');
const buttons = screen.getAllByText('Delete');
expect(buttons.length).toBe(2);
fireEvent.press(buttons[0]);
expect(getAllByText('Delete').length).toBe(1);
expect(screen.getAllByText('Delete').length).toBe(1);
});
});
```