From 6a97270babcd3e3ba7e185692593243eaeece00b Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 17 Oct 2022 22:32:00 +0200 Subject: [PATCH 1/6] docs: testing environment --- website/docs/TestingEnvironment.md | 159 +++++++++++++++++++++++++++++ website/sidebars.js | 14 +-- 2 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 website/docs/TestingEnvironment.md diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md new file mode 100644 index 000000000..3a9b77445 --- /dev/null +++ b/website/docs/TestingEnvironment.md @@ -0,0 +1,159 @@ +--- +id: testing-env +title: Testing Environment +--- + +## Testing Environment + +React Native Testing Library allows you to write integration and component tests for your React Native app or library. While the JSX code used in tests closely resembles your React Native app, the things are not quite simple as they might appear. In this document we will describe the key elements of our testing environment and highlight things to be aware when writing more advanced tests or diagnosing issues. + +### React renderers + +React allows you to write declarative code using JSX, write function or class components, or use hooks like `useState`. In order to output the results of your components it needs to work with a render. Every React app uses some type of renderer: React Native is a renderer for mobile apps, web apps use React DOM, and there are other more [specialised renderers](https://github.com/chentsulin/awesome-react-renderer) that can can e.g. render to console or HTML canvas. + +When you run your tests in React Native Testing Library, somewhat contrary to what the name suggest, they are actually **not** using React Native render. This is because this renderer needs to be run on iOS or Android operating system, so it would need to run on device or simulator. + +### React Test Render + +Instead, RNTL uses React Test Renderer which is a specialised renderer that allows rending to pure JavaScript objects without access to mobile OS, and can run in Node.js environment using Jest (or other JavaScript test runners). + +Using React Test Renders has both pros and cons. + +Benefits: + +- tests can run on most CIs (linux, etc) and do not require mobile device or emulator +- faster test execution +- light runtime environment + +Disadvantages: + +- Tests do not execute native code +- Tests are not aware of view state that would be managed by native components, e.g. focus, unmanaged text boxes, etc. +- Assertions do not operate on native view hierarchy +- Runtime behaviours are simulated, sometimes imperfectly + +It’s worth noting that React Testing Library (web one), works a bit different. While RTL also runs in Jest, it also has access to simulated browser DOM environment from jsdom package, so it can use a regular React DOM renderer. Unfortunately, there is no similar React Native runtime environment package. This is probably due to to the fact that while browser environment is well defined and highly standardised, the React Native environment is in constant evolution, in sync with the evolution of underlying OS-es. Maintaining such environment would require duplicating countless React Native behaviours, and keeping that in sync as React Native evolves. + +### Element tree + +Invoking `render()` functions results in creation of element tree. This is done internally by invoking `TestRendere.create()` method. The output tree represents your React Native component tree, each node of that tree is an “instance” of some React component (to be more precise: each node represents a React fiber, and only class components have instances, while function components store the hook state using fiber). + +These tree elements are represented by `ReactTestInstance` type: + +```jsx +// Based on: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer/index.d.ts +interface ReactTestInstance { + type: ElementType; + props: { [propName: string]: any }; + parent: ReactTestInstance | null; + children: Array; + + // Other props and methods +} +``` + +### Host and composite components + +One of the most important aspects of the element tree is that it is composed of both host and composite components: + +- [Host components]([https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components)) are components that will have direct counterparts in the native view tree. Typical examples are ``, `` , ``, and `` from React Native. You can think of these as analogue of `
`, `` etc on the Web. You can also create your own host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler. +- [Composite components]([https://reactnative.dev/architecture/glossary#react-composite-components](https://reactnative.dev/architecture/glossary#react-composite-components)) are React code organisation units that exist only on the JavaScript side of your app. Typical examples are components you create (both function and class components), components imported from React Native (`View`, `Text`, etc) or from 3rd party packages. + +That might sound a bit confusing at first, since we put React Native’s `View` in both categories. There are actually two `View` components: composite one and host one. The relation between them is as follows: + +- composite `View` is the type imported from `react-native` package. It’s a JavaScript component, which renderers host `View` as its only child in the element tree. +- host `View` , which you do not render directly. React Native takes the props you pass to the composite `View`, does some processing on them and passes them to host `View`. + +The part of the tree looks as follows: + +```jsx +* (composite) + * (host) + * children prop passed in JSX +``` + +Similar relation exists between other composite and host pairs: e.g. `Text` , `TextInput` and `Image` components: + +```jsx +* (composite) + * (host) + * string (or mixed) content +``` + +Not all React Native components are organised this way, e.g. when you use `Pressable` (or `TouchableOpacity`) there is no host `Pressable`, but composite `Pressable` is rendering a host `View` with certain props being set: + +```jsx +* (composite) + * (host) + * children prop passed in JSX +``` + +#### Differentiating between host and composite elements + +Any easy way to differentiate between host and composite elements is the `type` prop of `ReactTestInstance`: + +- for host components it’s always a string value representing component name, e.g. `"View"` +- for composite components it’s function or class defining the component + +You can use the following code to check if given element is a host one: + +```jsx +function isHostElement(element: ReactTestInstance) { + return typeof element.type === 'string'; +} +``` + +### Tree nodes + +We encourage you to only assert values on host views in your tests, because they represent the user interface view and controls that the user will be able to see and interact with. Users cannot see, or interact with, composite views as they exist purely in JavaScript domain and do not generate any visible UI. + +#### Asserting props + +As an example, if you make assertions on a `style` prop of a composite element, there is no guarantee that the style will be visible to the user, as the component author can forget to pass this prop to some underlying `View` or other host component. Similarly `onPress` event handler on a composite prop can be unreachable by the user. + +```jsx +function ForgotToPassPropsButton({ title, onPress, style }) { + return ( + + {title} + + ); +} +``` + +In the above example user defined components accepts both `onPress` and `style` props but does not pass it (through `Pressable`) to host views, so these props will not affect the user interface. + +### Tree navigation + +When navigating three using `parent` or `children` props of `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. + +If you want to find a host element for given element they you might use following code: + +```jsx +function getHostParent(element: ReactTestInstance) { + let current = element.parent; + while (current) { + if (isHostElement(current)) { + return current; + } + + current = current.parent; + } + + return null; +} +``` + +### Queries + +Most of the Testing Library queries return host components, in order to encourage best practices described above. + +At this stage, there are some noteworthy exceptions: + +- `*ByText` queries returns composite `Text` element +- `*ByDisplayValue` queries returns composite `TextInput` element +- `*ByPlaceholderText` queries returns composite `TextInput` element + +This will change in the near future, as we make efforts for all queries to return host components. Meanwhile it should be a huge issue, as composite `Text` and `TextInput`generally pass their props down to host counterparts. + +Additionally, `UNSAFE_*ByType` and `UNSAFE_*ByProps` queries can return both host and composite components depending on used predicates. They are marked as unsafe precisely because testing composite components makes your test more fragile. diff --git a/website/sidebars.js b/website/sidebars.js index ec1738b48..a4cdbf2fc 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -1,17 +1,19 @@ module.exports = { docs: { - Introduction: ['getting-started'], + Introduction: ['getting-started', 'faq'], 'API Reference': ['api', 'api-queries'], Guides: [ + 'troubleshooting', + 'how-should-i-query', + 'testing-env', + 'eslint-plugin-testing-library', + 'understanding-act', + ], + Migrations: [ 'migration-v11', 'migration-v9', 'migration-v7', 'migration-v2', - 'how-should-i-query', - 'faq', - 'troubleshooting', - 'eslint-plugin-testing-library', - 'understanding-act', ], Examples: ['react-navigation', 'redux-integration'], }, From f9c100b5df4ca0f68c5fc026e530706600a62c5e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 21 Oct 2022 12:11:02 +0200 Subject: [PATCH 2/6] fix: links --- website/docs/TestingEnvironment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md index 3a9b77445..6025455ed 100644 --- a/website/docs/TestingEnvironment.md +++ b/website/docs/TestingEnvironment.md @@ -56,8 +56,8 @@ interface ReactTestInstance { One of the most important aspects of the element tree is that it is composed of both host and composite components: -- [Host components]([https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components)) are components that will have direct counterparts in the native view tree. Typical examples are ``, `` , ``, and `` from React Native. You can think of these as analogue of `
`, `` etc on the Web. You can also create your own host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler. -- [Composite components]([https://reactnative.dev/architecture/glossary#react-composite-components](https://reactnative.dev/architecture/glossary#react-composite-components)) are React code organisation units that exist only on the JavaScript side of your app. Typical examples are components you create (both function and class components), components imported from React Native (`View`, `Text`, etc) or from 3rd party packages. +- [Host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components) are components that will have direct counterparts in the native view tree. Typical examples are ``, `` , ``, and `` from React Native. You can think of these as analogue of `
`, `` etc on the Web. You can also create your own host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler. +- [Composite components](https://reactnative.dev/architecture/glossary#react-composite-components) are React code organisation units that exist only on the JavaScript side of your app. Typical examples are components you create (both function and class components), components imported from React Native (`View`, `Text`, etc) or from 3rd party packages. That might sound a bit confusing at first, since we put React Native’s `View` in both categories. There are actually two `View` components: composite one and host one. The relation between them is as follows: From 466c4da55a455fd601a0f9461644ddb76fad4265 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 21 Oct 2022 12:13:07 +0200 Subject: [PATCH 3/6] docs: link from FAQ --- website/docs/FAQ.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/docs/FAQ.md b/website/docs/FAQ.md index 078650055..96001a5bc 100644 --- a/website/docs/FAQ.md +++ b/website/docs/FAQ.md @@ -6,7 +6,6 @@ title: FAQ
Can I test native features of React Native apps? -

Short answer: no.

React Native Testing Library does not provide a full React Native runtime since that would require running on physical device @@ -16,6 +15,8 @@ Instead of using React Native renderer, it simulates only the JavaScript part of using [React Test Renderer](https://reactjs.org/docs/test-renderer.html) while providing queries and `fireEvent` APIs that mimick certain behaviors from the real runtime. +You can learn more about our testing environment [here](./TestingEnvironment.md). + This approach has certain benefits and shortfalls. On the positive side: - it allows testing most of the logic of regular React Native apps @@ -35,8 +36,6 @@ For instance, [react-native's ScrollView](https://reactnative.dev/docs/scrollvie
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. From 4958af4bdfefaa73b1a2b951114b8817862bb4ab Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 21 Oct 2022 19:09:13 +0200 Subject: [PATCH 4/6] Update TestingEnvironment.md --- website/docs/TestingEnvironment.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md index 6025455ed..b98bee350 100644 --- a/website/docs/TestingEnvironment.md +++ b/website/docs/TestingEnvironment.md @@ -41,7 +41,6 @@ Invoking `render()` functions results in creation of element tree. This is done These tree elements are represented by `ReactTestInstance` type: ```jsx -// Based on: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer/index.d.ts interface ReactTestInstance { type: ElementType; props: { [propName: string]: any }; @@ -52,6 +51,8 @@ interface ReactTestInstance { } ``` +Based on: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer/index.d.ts + ### Host and composite components One of the most important aspects of the element tree is that it is composed of both host and composite components: @@ -68,7 +69,7 @@ The part of the tree looks as follows: ```jsx * (composite) - * (host) + * (host) * children prop passed in JSX ``` @@ -76,7 +77,7 @@ Similar relation exists between other composite and host pairs: e.g. `Text` , `T ```jsx * (composite) - * (host) + * (host) * string (or mixed) content ``` @@ -84,7 +85,7 @@ Not all React Native components are organised this way, e.g. when you use `Press ```jsx * (composite) - * (host) + * (host) * children prop passed in JSX ``` From b0a0f4a464017424016e12a72dbace8fc1ecd7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 28 Oct 2022 16:44:24 +0200 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pierre Zimmermann <64224599+pierrezimmermannbam@users.noreply.github.com> Co-authored-by: Augustin Le Fèvre --- website/docs/TestingEnvironment.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md index b98bee350..d92b32de7 100644 --- a/website/docs/TestingEnvironment.md +++ b/website/docs/TestingEnvironment.md @@ -5,23 +5,23 @@ title: Testing Environment ## Testing Environment -React Native Testing Library allows you to write integration and component tests for your React Native app or library. While the JSX code used in tests closely resembles your React Native app, the things are not quite simple as they might appear. In this document we will describe the key elements of our testing environment and highlight things to be aware when writing more advanced tests or diagnosing issues. +React Native Testing Library allows you to write integration and component tests for your React Native app or library. While the JSX code used in tests closely resembles your React Native app, the things are not quite as simple as they might appear. In this document we will describe the key elements of our testing environment and highlight things to be aware of when writing more advanced tests or diagnosing issues. ### React renderers -React allows you to write declarative code using JSX, write function or class components, or use hooks like `useState`. In order to output the results of your components it needs to work with a render. Every React app uses some type of renderer: React Native is a renderer for mobile apps, web apps use React DOM, and there are other more [specialised renderers](https://github.com/chentsulin/awesome-react-renderer) that can can e.g. render to console or HTML canvas. +React allows you to write declarative code using JSX, write function or class components, or use hooks like `useState`. In order to output the results of your components it needs to work with a renderer. Every React app uses some type of renderer: React Native is a renderer for mobile apps, web apps use React DOM, and there are other more [specialised renderers](https://github.com/chentsulin/awesome-react-renderer) that can e.g. render to console or HTML canvas. -When you run your tests in React Native Testing Library, somewhat contrary to what the name suggest, they are actually **not** using React Native render. This is because this renderer needs to be run on iOS or Android operating system, so it would need to run on device or simulator. +When you run your tests in React Native Testing Library, somewhat contrary to what the name suggest, they are actually **not** using React Native renderer. This is because this renderer needs to be run on iOS or Android operating system, so it would need to run on device or simulator. -### React Test Render +### React Test Renderer -Instead, RNTL uses React Test Renderer which is a specialised renderer that allows rending to pure JavaScript objects without access to mobile OS, and can run in Node.js environment using Jest (or other JavaScript test runners). +Instead, RNTL uses React Test Renderer which is a specialised renderer that allows rendering to pure JavaScript objects without access to mobile OS, and that can run in a Node.js environment using Jest (or any other JavaScript test runner). -Using React Test Renders has both pros and cons. +Using React Test Renderer has pros and cons. Benefits: -- tests can run on most CIs (linux, etc) and do not require mobile device or emulator +- tests can run on most CIs (linux, etc) and do not require a mobile device or emulator - faster test execution - light runtime environment @@ -36,11 +36,11 @@ It’s worth noting that React Testing Library (web one), works a bit different. ### Element tree -Invoking `render()` functions results in creation of element tree. This is done internally by invoking `TestRendere.create()` method. The output tree represents your React Native component tree, each node of that tree is an “instance” of some React component (to be more precise: each node represents a React fiber, and only class components have instances, while function components store the hook state using fiber). +Invoking `render()` function results in creation of an element tree. This is done internally by invoking `TestRenderer.create()` method. The output tree represents your React Native component tree, each node of that tree is an “instance” of some React component (to be more precise: each node represents a React fiber, and only class components have instances, while function components store the hook state using fiber). These tree elements are represented by `ReactTestInstance` type: -```jsx +```tsx interface ReactTestInstance { type: ElementType; props: { [propName: string]: any }; @@ -62,7 +62,7 @@ One of the most important aspects of the element tree is that it is composed of That might sound a bit confusing at first, since we put React Native’s `View` in both categories. There are actually two `View` components: composite one and host one. The relation between them is as follows: -- composite `View` is the type imported from `react-native` package. It’s a JavaScript component, which renderers host `View` as its only child in the element tree. +- composite `View` is the type imported from `react-native` package. It’s a JavaScript component, which renders host `View` as its only child in the element tree. - host `View` , which you do not render directly. React Native takes the props you pass to the composite `View`, does some processing on them and passes them to host `View`. The part of the tree looks as follows: @@ -94,7 +94,7 @@ Not all React Native components are organised this way, e.g. when you use `Press Any easy way to differentiate between host and composite elements is the `type` prop of `ReactTestInstance`: - for host components it’s always a string value representing component name, e.g. `"View"` -- for composite components it’s function or class defining the component +- for composite components it’s a function or class corresponding to the component You can use the following code to check if given element is a host one: @@ -126,7 +126,7 @@ In the above example user defined components accepts both `onPress` and `style` ### Tree navigation -When navigating three using `parent` or `children` props of `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. +When navigating a tree of react elements using `parent` or `children` props of a `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. If you want to find a host element for given element they you might use following code: @@ -155,6 +155,6 @@ At this stage, there are some noteworthy exceptions: - `*ByDisplayValue` queries returns composite `TextInput` element - `*ByPlaceholderText` queries returns composite `TextInput` element -This will change in the near future, as we make efforts for all queries to return host components. Meanwhile it should be a huge issue, as composite `Text` and `TextInput`generally pass their props down to host counterparts. +This will change in the near future, as we make efforts for all queries to return host components. Meanwhile it shouldn't be a huge issue, as composite `Text` and `TextInput`generally pass their props down to host counterparts. Additionally, `UNSAFE_*ByType` and `UNSAFE_*ByProps` queries can return both host and composite components depending on used predicates. They are marked as unsafe precisely because testing composite components makes your test more fragile. From dd08cb5e80ace7a4c8ab05a8506a2e08a451c6e9 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 31 Oct 2022 11:46:36 +0100 Subject: [PATCH 6/6] docs: code review changes --- website/docs/TestingEnvironment.md | 28 +++++++++++----------------- website/sidebars.js | 3 +-- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/website/docs/TestingEnvironment.md b/website/docs/TestingEnvironment.md index d92b32de7..155c29420 100644 --- a/website/docs/TestingEnvironment.md +++ b/website/docs/TestingEnvironment.md @@ -3,6 +3,11 @@ id: testing-env title: Testing Environment --- +:::info +This document is intended for more advanced audience. You should be able to write integration or component tests without reading this. +It is intended for people who want to better understand internals of our testing environment, e.g. in order to contribute to the codebase. +::: + ## Testing Environment React Native Testing Library allows you to write integration and component tests for your React Native app or library. While the JSX code used in tests closely resembles your React Native app, the things are not quite as simple as they might appear. In this document we will describe the key elements of our testing environment and highlight things to be aware of when writing more advanced tests or diagnosing issues. @@ -122,28 +127,17 @@ function ForgotToPassPropsButton({ title, onPress, style }) { } ``` -In the above example user defined components accepts both `onPress` and `style` props but does not pass it (through `Pressable`) to host views, so these props will not affect the user interface. +In the above example user defined components accepts both `onPress` and `style` props but does not pass it (through `Pressable`) to host views, so these props will not affect the user interface. Additionally, React Native and other libraries might pass some of the props under different names or transform their values between composite and host components. ### Tree navigation -When navigating a tree of react elements using `parent` or `children` props of a `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. - -If you want to find a host element for given element they you might use following code: - -```jsx -function getHostParent(element: ReactTestInstance) { - let current = element.parent; - while (current) { - if (isHostElement(current)) { - return current; - } +:::caution +You should avoid navigating over element tree, as this makes your testing code fragile and may result in false positives. This section is more relevant for people how want to contribute to our codebase. +::: - current = current.parent; - } +When navigating a tree of react elements using `parent` or `children` props of a `ReactTestInstance` element, you will encounter both host and composite elements. You should be careful when navigating the element tree, as the tree structure for 3rd party components and change independently from your code and cause unexpected test failures. - return null; -} -``` +Inside RNTL we have various tree navigation helpers: `getHostParent`, `getHostChildren`, etc. These are intentionally not exported as using them is not a recommended practice. ### Queries diff --git a/website/sidebars.js b/website/sidebars.js index a4cdbf2fc..9b1f176b5 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -5,10 +5,9 @@ module.exports = { Guides: [ 'troubleshooting', 'how-should-i-query', - 'testing-env', 'eslint-plugin-testing-library', - 'understanding-act', ], + Advanced: ['testing-env', 'understanding-act'], Migrations: [ 'migration-v11', 'migration-v9',