From 1cd0608f7302c9f493d260cd6f857241c8ccd902 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 23 Jun 2022 23:38:53 +0200 Subject: [PATCH 1/8] feature: screen --- src/__tests__/screen.test.tsx | 39 ++++++++++++ src/cleanup.ts | 2 + src/pure.ts | 6 +- src/render.tsx | 8 ++- src/screen.ts | 108 ++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/screen.test.tsx create mode 100644 src/screen.ts diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx new file mode 100644 index 000000000..c34828dce --- /dev/null +++ b/src/__tests__/screen.test.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { Text } from 'react-native'; +import { render, screen } from '..'; + +test('screen has the same queries as render result', () => { + const result = render(Mt. Everest); + 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(Mt. Everest); + render(Mt. Blanc); + const finalResult = render(Śnieżka); + expect(screen).toBe(finalResult); + + expect(screen.getByText('Śnieżka')).toBeTruthy(); + expect(screen.queryByText('Mt. Everest')).toBeFalsy(); + expect(screen.queryByText('Mt. Blanc')).toBeFalsy(); +}); + +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' + ); +}); diff --git a/src/cleanup.ts b/src/cleanup.ts index 04cdfa208..681d22a47 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -1,9 +1,11 @@ import * as React from 'react'; +import { clearRenderResult } from './screen'; type CleanUpFunction = (nextElement?: React.ReactElement) => void; let cleanupQueue = new Set(); export default function cleanup() { + clearRenderResult(); cleanupQueue.forEach((fn) => fn()); cleanupQueue.clear(); } diff --git a/src/pure.ts b/src/pure.ts index 32d51c0e1..c3401abc2 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,20 +1,22 @@ 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 { act }; export { cleanup }; export { fireEvent }; -export { render }; +export { render, RenderResult }; export { waitFor }; export { waitForElementToBeRemoved }; export { within, getQueriesForElement }; export { getDefaultNormalizer }; export { renderHook }; +export { screen }; export type RenderAPI = ReturnType; diff --git a/src/render.tsx b/src/render.tsx index e674ede01..7f8142c60 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -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; @@ -15,6 +16,8 @@ type TestRendererOptions = { createNodeMock: (element: React.ReactElement) => any; }; +export type RenderResult = ReturnType; + /** * Renders test component deeply using react-test-renderer and exposes helpers * to assert on the output. @@ -40,7 +43,7 @@ export default function render( addToCleanupQueue(unmount); - return { + const result = { ...getQueriesForElement(instance), update, unmount, @@ -49,6 +52,9 @@ export default function render( toJSON: renderer.toJSON, debug: debug(instance, renderer), }; + + setRenderResult(result); + return result; } function renderWithAct( diff --git a/src/screen.ts b/src/screen.ts new file mode 100644 index 000000000..6d44957e4 --- /dev/null +++ b/src/screen.ts @@ -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; +} From 719975e28fdd6377e6966bd85ff868f6c6219408 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 00:29:59 +0200 Subject: [PATCH 2/8] feature: update docs --- README.md | 10 ++-- website/docs/API.md | 89 ++++++++++++++++++-------------- website/docs/GettingStarted.md | 10 ++-- website/docs/Queries.md | 86 +++++++++++++++--------------- website/docs/ReactNavigation.md | 30 +++++------ website/docs/ReduxIntegration.md | 22 ++++---- 6 files changed, 129 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index d929418e8..6560a836e 100644 --- a/README.md +++ b/README.md @@ -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( - - ); + render(); - 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' }, diff --git a/website/docs/API.md b/website/docs/API.md index cbaf87f1a..ce517b547 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -6,9 +6,12 @@ title: API ### Table of contents: - [`render`](#render) + - [`...queries`](#queries) + - [Example](#example) - [`update`](#update) - [`unmount`](#unmount) - [`debug`](#debug) + - [`debug.shallow`](#debugshallow) - [`toJSON`](#tojson) - [`container`](#container) - [`cleanup`](#cleanup) @@ -17,19 +20,26 @@ title: API - [`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 +68,8 @@ import { render } from '@testing-library/react-native'; import { QuestionsBoard } from '../QuestionsBoard'; test('should verify two questions', () => { - const { queryAllByRole } = render(); - const allQuestions = queryAllByRole('header'); + render(); + const allQuestions = screen.queryAllByRole('header'); expect(allQuestions).toHaveLength(2); }); @@ -117,9 +127,9 @@ debug(message?: string): void Pretty prints deeply rendered component passed to `render` with optional message on top. ```jsx -const { debug } = render(); +render(); -debug('optional message'); +screen.debug('optional message'); ``` logs optional message and colored JSX: @@ -154,6 +164,12 @@ container: ReactTestInstance; A reference to the rendered root element. +:::info +Last `render` result is kept in `screen` variable that you can import from `@testing-library/react-native` package. + +Using screen instead of destructuring `render` result is recommended apprach. See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details. +::: + ## `cleanup` ```ts @@ -208,17 +224,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 ); - fireEvent(getByPlaceholderText('change'), 'onChangeText', 'ab'); + fireEvent(screen.getByPlaceholderText('change'), 'onChangeText', 'ab'); expect(onEventMock).toHaveBeenCalledWith('ab'); }); ``` @@ -235,14 +251,14 @@ import { fireEvent, render } from '@testing-library/react-native'; const onBlurMock = jest.fn(); -const { getByPlaceholderText } = render( +render( ); // you can omit the `on` prefix -fireEvent(getByPlaceholderText('my placeholder'), 'blur'); +fireEvent(screen.getByPlaceholderText('my placeholder'), 'blur'); ``` ## `fireEvent[eventName]` @@ -263,7 +279,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 +289,7 @@ const eventData = { }, }; -const { getByText } = render( +render( Press me @@ -281,7 +297,7 @@ const { getByText } = render( ); -fireEvent.press(getByText('Press me'), eventData); +fireEvent.press(screen.getByText('Press me'), eventData); expect(onPressMock).toHaveBeenCalledWith(eventData); ``` @@ -295,18 +311,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( ); -fireEvent.changeText(getByPlaceholderText('Enter data'), CHANGE_TEXT); +fireEvent.changeText(screen.getByPlaceholderText('Enter data'), CHANGE_TEXT); ``` ### `fireEvent.scroll` @@ -321,7 +337,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 +348,23 @@ const eventData = { }, }; -const { getByText } = render( +render( XD ); -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( ({ key: `${key}` }))} renderItem={() => } @@ -375,7 +391,7 @@ const eventData = { }, }; -fireEvent.scroll(getByTestId('flat-list'), eventData); +fireEvent.scroll(screen.getByTestId('flat-list'), eventData); expect(onEndReached).toHaveBeenCalled(); ``` @@ -399,12 +415,12 @@ function waitFor( 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(); + render(); - await waitFor(() => getByText('Banana ready')); + await waitFor(() => screen.getByText('Banana ready')); }); ``` @@ -428,15 +444,12 @@ function waitForElementToBeRemoved( 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(); + render(); - await waitForElementToBeRemoved(() => getByText('Banana ready')); + await waitForElementToBeRemoved(() => screen.getByText('Banana ready')); }); ``` @@ -466,7 +479,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 +496,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(
); -const submitButton = queryByText('submit'); +render(); +const submitButton = screen.queryByText('submit'); expect(submitButton).toBeNull(); // it doesn't exist ``` @@ -497,8 +510,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(); -const submitButtons = queryAllByText('submit'); +render(); +const submitButtons = screen.queryAllByText('submit'); expect(submitButtons).toHaveLength(3); // expect 3 elements ``` diff --git a/website/docs/GettingStarted.md b/website/docs/GettingStarted.md index f88e7513b..2110ec98c 100644 --- a/website/docs/GettingStarted.md +++ b/website/docs/GettingStarted.md @@ -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( - - ); + render(); - 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' }, diff --git a/website/docs/Queries.md b/website/docs/Queries.md index c848e3de8..b0f13b23f 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -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 @@ -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(); -const element = getByLabelText('my-label'); +render(); +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(); -const element = getByHintText('Plays a song'); +render(); +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(); -const element = getByA11yStates(['checked']); -const element2 = getByA11yStates('checked'); +render(); +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(); -const element = getByRole('button'); +render(); +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(); -const element = getByA11yState({ disabled: true }); +render(); +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(); -const element = getByA11yValue({ min: 40 }); +render(); +const element = screen.getByA11yValue({ min: 40 }); ``` ## TextMatch @@ -248,34 +251,34 @@ type TextMatchOptions = { Given the following render: ```jsx -const { getByText } = render(Hello World); +render(Hello World); ``` 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, ''), }); ``` diff --git a/website/docs/ReactNavigation.md b/website/docs/ReactNavigation.md index b6a519de5..a7c942c20 100644 --- a/website/docs/ReactNavigation.md +++ b/website/docs/ReactNavigation.md @@ -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', () => { ); - 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', () => { ); - 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', () => { ); - 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', () => { ); - 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(); }); diff --git a/website/docs/ReduxIntegration.md b/website/docs/ReduxIntegration.md index 704f35f52..18bc20aca 100644 --- a/website/docs/ReduxIntegration.md +++ b/website/docs/ReduxIntegration.md @@ -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', () => { ); - 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', () => { ); - 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', () => { ); - 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); }); }); ``` From a1c7d6ac492d18f7cb4e47530bd7f596046e964f Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 00:32:31 +0200 Subject: [PATCH 3/8] refactor: add flow type --- typings/index.flow.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.flow.js b/typings/index.flow.js index 286ecb6f9..ead24d516 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -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; From 68654f416a7f52ef2444a2ba29ee2b0cd4b7f5bb Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 00:45:14 +0200 Subject: [PATCH 4/8] docs: more --- website/docs/API.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/website/docs/API.md b/website/docs/API.md index ce517b547..dc61df719 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -14,6 +14,7 @@ title: API - [`debug.shallow`](#debugshallow) - [`toJSON`](#tojson) - [`container`](#container) +- [`screen`](#screen) - [`cleanup`](#cleanup) - [`fireEvent`](#fireevent) - [`fireEvent[eventName]`](#fireeventeventname) @@ -77,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` @@ -164,11 +171,15 @@ container: ReactTestInstance; A reference to the rendered root element. -:::info -Last `render` result is kept in `screen` variable that you can import from `@testing-library/react-native` package. +## `screen` -Using screen instead of destructuring `render` result is recommended apprach. See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details. -::: +```ts +let screen: RenderResult; +``` + +Hold the value of latest render call for easier access to query and other functions returned by [`render`](#render). + +It's 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` @@ -176,7 +187,7 @@ Using screen instead of destructuring `render` result is recommended apprach. Se 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. @@ -211,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 From 8ab31c1a6aada0f70b550464f83961d7b4402a79 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 00:50:32 +0200 Subject: [PATCH 5/8] docs: final tweaks --- website/docs/EslintPLluginTestingLibrary.md | 2 -- website/docs/FAQ.md | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/website/docs/EslintPLluginTestingLibrary.md b/website/docs/EslintPLluginTestingLibrary.md index 43419a863..2a4c934cc 100644 --- a/website/docs/EslintPLluginTestingLibrary.md +++ b/website/docs/EslintPLluginTestingLibrary.md @@ -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: diff --git a/website/docs/FAQ.md b/website/docs/FAQ.md index 6e2f7e331..078650055 100644 --- a/website/docs/FAQ.md +++ b/website/docs/FAQ.md @@ -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. + +
+ Should I use/migrate to `screen` queries? + +
+ +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. + +
From 9b0e6e931bdf48d28b628329337dc01f562f24b8 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 11:44:27 +0200 Subject: [PATCH 6/8] test rerender --- src/__tests__/screen.test.tsx | 30 +++++++++++++++++++++++++++++- website/docs/Queries.md | 24 ++++++++++++------------ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/__tests__/screen.test.tsx b/src/__tests__/screen.test.tsx index c34828dce..dbde73551 100644 --- a/src/__tests__/screen.test.tsx +++ b/src/__tests__/screen.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Text } from 'react-native'; +import { View, Text } from 'react-native'; import { render, screen } from '..'; test('screen has the same queries as render result', () => { @@ -23,6 +23,34 @@ test('screen holds last render result', () => { expect(screen.queryByText('Mt. Blanc')).toBeFalsy(); }); +test('screen works with updating rerender', () => { + const result = render(Mt. Everest); + expect(screen).toBe(result); + + screen.rerender(Śnieżka); + expect(screen).toBe(result); + expect(screen.getByText('Śnieżka')).toBeTruthy(); +}); + +test('screen works with nested re-mounting rerender', () => { + const result = render( + + Mt. Everest + + ); + expect(screen).toBe(result); + + screen.rerender( + + + Śnieżka + + + ); + 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' diff --git a/website/docs/Queries.md b/website/docs/Queries.md index b0f13b23f..d1409e76c 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -96,10 +96,10 @@ Returns a `ReactTestInstance` with matching text – may be a string or regular This method will join `` 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(); -const element = getByText('banana'); +render(); +const element = screen.getByText('banana'); ``` ### `ByPlaceholderText` @@ -109,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(); -const element = getByPlaceholderText('username'); +render(); +const element = screen.getByPlaceholderText('username'); ``` ### `ByDisplayValue` @@ -122,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(); -const element = getByDisplayValue('username'); +render(); +const element = screen.getByDisplayValue('username'); ``` ### `ByTestId` @@ -135,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(); -const element = getByTestId('unique-id'); +render(); +const element = screen.getByTestId('unique-id'); ``` :::info From b046b50e3d20743acd4314d99ef7816c1d1eef73 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 24 Jun 2022 12:03:20 +0200 Subject: [PATCH 7/8] code review changes --- src/pure.ts | 4 +++- website/docs/API.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index c3401abc2..94c939803 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -9,10 +9,12 @@ import { getDefaultNormalizer } from './matches'; import { renderHook } from './renderHook'; import { screen } from './screen'; +export type { RenderResult }; + export { act }; export { cleanup }; export { fireEvent }; -export { render, RenderResult }; +export { render }; export { waitFor }; export { waitForElementToBeRemoved }; export { within, getQueriesForElement }; diff --git a/website/docs/API.md b/website/docs/API.md index dc61df719..e634f44d6 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -179,7 +179,7 @@ let screen: RenderResult; Hold the value of latest render call for easier access to query and other functions returned by [`render`](#render). -It's 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. +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` From e9c5d3498957517102aee96840b28e638d31b478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 24 Jun 2022 13:14:05 +0200 Subject: [PATCH 8/8] alias RenderResult to RenderAPI --- src/pure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pure.ts b/src/pure.ts index 94c939803..34e48f8d7 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -10,6 +10,7 @@ import { renderHook } from './renderHook'; import { screen } from './screen'; export type { RenderResult }; +export type RenderAPI = RenderResult; export { act }; export { cleanup }; @@ -21,4 +22,3 @@ export { within, getQueriesForElement }; export { getDefaultNormalizer }; export { renderHook }; export { screen }; -export type RenderAPI = ReturnType;