├── .all-contributorsrc
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── Bug_Report.md
│ ├── Feature_Request.md
│ └── Question.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmrc
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── babel.config.js
├── cleanup-after-each.js
├── examples
├── __tests__
│ ├── input-event.js
│ ├── react-context.js
│ ├── react-intl.js
│ ├── react-navigation.js
│ ├── react-redux.js
│ ├── update-props.js
│ └── use-hooks.js
└── react-context.js
├── jest-preset.js
├── jest.config.js
├── other
├── cheat-sheet.pdf
└── whale.png
├── package.json
├── pure.js
├── src
├── __tests__
│ ├── __snapshots__
│ │ ├── fetch.js.snap
│ │ ├── misc.js.snap
│ │ └── render.js.snap
│ ├── act.js
│ ├── bugs.js
│ ├── debug.js
│ ├── end-to-end.js
│ ├── events.js
│ ├── fetch.js
│ ├── forms.js
│ ├── misc.js
│ ├── new-act.js
│ ├── no-act.js
│ ├── old-act.js
│ ├── render.js
│ ├── rerender.js
│ └── stopwatch.js
├── act-compat.js
├── cleanup-async.js
├── index.js
├── lib
│ ├── __tests__
│ │ ├── __snapshots__
│ │ │ └── to-json.js.snap
│ │ ├── config.js
│ │ ├── get-by-errors.js
│ │ ├── helpers.js
│ │ ├── matches.js
│ │ ├── misc.js
│ │ ├── pretty-print.js
│ │ ├── queries.find.js
│ │ ├── query-helpers.js
│ │ ├── text-matchers.js
│ │ ├── to-json.js
│ │ ├── wait-for-element-to-be-removed.fake-timers.js
│ │ ├── wait-for-element-to-be-removed.js
│ │ ├── wait-for-element.js
│ │ ├── wait.js
│ │ └── within.js
│ ├── config.js
│ ├── events.js
│ ├── get-node-text.js
│ ├── get-queries-for-element.js
│ ├── helpers.js
│ ├── index.js
│ ├── matches.js
│ ├── pretty-print.js
│ ├── queries
│ │ ├── all-utils.js
│ │ ├── display-value.js
│ │ ├── hint-text.js
│ │ ├── index.js
│ │ ├── label-text.js
│ │ ├── placeholder-text.js
│ │ ├── role.js
│ │ ├── test-id.js
│ │ ├── text.js
│ │ └── title.js
│ ├── query-helpers.js
│ ├── to-json.js
│ ├── wait-for-element-to-be-removed.js
│ ├── wait-for-element.js
│ └── wait.js
└── preset
│ ├── configure.js
│ ├── mock-component.js
│ ├── mock-modules.js
│ ├── mock-native-methods.js
│ ├── mock-refresh-control.js
│ ├── mock-scroll-view.js
│ ├── serializer.js
│ └── setup.js
└── typings
├── config.d.ts
├── events.d.ts
├── get-node-text.d.ts
├── get-queries-for-element.d.ts
├── index.d.ts
├── matches.d.ts
├── pretty-print.d.ts
├── queries.d.ts
├── query-helpers.d.ts
├── to-json.d.ts
├── wait-for-element-to-be-removed.d.ts
├── wait-for-element.d.ts
└── wait.d.ts
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "native-testing-library",
3 | "projectOwner": "testing-library",
4 | "repoType": "github",
5 | "files": [
6 | "README.md"
7 | ],
8 | "imageSize": 100,
9 | "commit": true,
10 | "commitConvention": "angular",
11 | "contributors": [
12 | {
13 | "login": "bcarroll22",
14 | "name": "Brandon Carroll",
15 | "avatar_url": "https://avatars2.githubusercontent.com/u/11020406?v=4",
16 | "profile": "https://github.com/bcarroll22",
17 | "contributions": [
18 | "code",
19 | "doc",
20 | "infra",
21 | "test"
22 | ]
23 | },
24 | {
25 | "login": "TAGraves",
26 | "name": "Tommy Graves",
27 | "avatar_url": "https://avatars1.githubusercontent.com/u/2263711?v=4",
28 | "profile": "http://tagraves.com",
29 | "contributions": [
30 | "ideas",
31 | "maintenance",
32 | "review"
33 | ]
34 | },
35 | {
36 | "login": "kentcdodds",
37 | "name": "Kent C. Dodds",
38 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
39 | "profile": "https://kentcdodds.com",
40 | "contributions": [
41 | "ideas"
42 | ]
43 | },
44 | {
45 | "login": "sz-piotr",
46 | "name": "Piotr Szlachciak",
47 | "avatar_url": "https://avatars2.githubusercontent.com/u/17070569?v=4",
48 | "profile": "https://github.com/sz-piotr",
49 | "contributions": [
50 | "code"
51 | ]
52 | },
53 | {
54 | "login": "mcgloneleviROOT",
55 | "name": "mcgloneleviROOT",
56 | "avatar_url": "https://avatars3.githubusercontent.com/u/48258981?v=4",
57 | "profile": "https://github.com/mcgloneleviROOT",
58 | "contributions": [
59 | "bug",
60 | "code"
61 | ]
62 | },
63 | {
64 | "login": "wolverineks",
65 | "name": "Kevin Sullivan",
66 | "avatar_url": "https://avatars2.githubusercontent.com/u/8462274?v=4",
67 | "profile": "http://exercism.io/profiles/wolverineks/619ce225090a43cb891d2edcbbf50401",
68 | "contributions": [
69 | "doc"
70 | ]
71 | },
72 | {
73 | "login": "elyalvarado",
74 | "name": "Ely Alvarado",
75 | "avatar_url": "https://avatars1.githubusercontent.com/u/545352?v=4",
76 | "profile": "https://github.com/elyalvarado",
77 | "contributions": [
78 | "code"
79 | ]
80 | },
81 | {
82 | "login": "lewie9021",
83 | "name": "Lewis Barnes",
84 | "avatar_url": "https://avatars3.githubusercontent.com/u/4729411?v=4",
85 | "profile": "https://github.com/lewie9021",
86 | "contributions": [
87 | "code",
88 | "question"
89 | ]
90 | },
91 | {
92 | "login": "mAAdhaTTah",
93 | "name": "James DiGioia",
94 | "avatar_url": "https://avatars0.githubusercontent.com/u/4371429?v=4",
95 | "profile": "http://jamesdigioia.com",
96 | "contributions": [
97 | "code"
98 | ]
99 | },
100 | {
101 | "login": "manakuro",
102 | "name": "mana",
103 | "avatar_url": "https://avatars1.githubusercontent.com/u/11571318?v=4",
104 | "profile": "https://manatoworks.me",
105 | "contributions": [
106 | "code"
107 | ]
108 | },
109 | {
110 | "login": "mateusz1913",
111 | "name": "Mateusz Mędrek",
112 | "avatar_url": "https://avatars2.githubusercontent.com/u/25980166?v=4",
113 | "profile": "https://github.com/mateusz1913",
114 | "contributions": [
115 | "code"
116 | ]
117 | },
118 | {
119 | "login": "smakosh",
120 | "name": "Ismail Ghallou ",
121 | "avatar_url": "https://avatars0.githubusercontent.com/u/20082141?v=4",
122 | "profile": "https://smakosh.com",
123 | "contributions": [
124 | "doc"
125 | ]
126 | },
127 | {
128 | "login": "jeffreyffs",
129 | "name": "jeffreyffs",
130 | "avatar_url": "https://avatars1.githubusercontent.com/u/1441462?v=4",
131 | "profile": "https://github.com/jeffreyffs",
132 | "contributions": [
133 | "code"
134 | ]
135 | },
136 | {
137 | "login": "SophieAu",
138 | "name": "Sophie Au",
139 | "avatar_url": "https://avatars2.githubusercontent.com/u/11145039?v=4",
140 | "profile": "https://www.sophieau.com/",
141 | "contributions": [
142 | "code"
143 | ]
144 | },
145 | {
146 | "login": "ajsmth",
147 | "name": "andy",
148 | "avatar_url": "https://avatars2.githubusercontent.com/u/40680668?v=4",
149 | "profile": "http://ajsmth.com",
150 | "contributions": [
151 | "code",
152 | "doc"
153 | ]
154 | },
155 | {
156 | "login": "aiham",
157 | "name": "Aiham",
158 | "avatar_url": "https://avatars2.githubusercontent.com/u/609164?v=4",
159 | "profile": "https://github.com/aiham",
160 | "contributions": [
161 | "code"
162 | ]
163 | },
164 | {
165 | "login": "sibelius",
166 | "name": "Sibelius Seraphini",
167 | "avatar_url": "https://avatars3.githubusercontent.com/u/2005841?v=4",
168 | "profile": "https://twitter.com/sseraphini",
169 | "contributions": [
170 | "code"
171 | ]
172 | },
173 | {
174 | "login": "AEgan",
175 | "name": "Alex Egan",
176 | "avatar_url": "https://avatars0.githubusercontent.com/u/3501927?v=4",
177 | "profile": "https://github.com/AEgan",
178 | "contributions": [
179 | "code"
180 | ]
181 | },
182 | {
183 | "login": "daveols",
184 | "name": "Dave Olsen",
185 | "avatar_url": "https://avatars3.githubusercontent.com/u/10344370?v=4",
186 | "profile": "http://daveolsen.com.au",
187 | "contributions": [
188 | "code",
189 | "test",
190 | "doc"
191 | ]
192 | }
193 | ],
194 | "contributorsPerLine": 7,
195 | "repoHost": "https://github.com",
196 | "skipCi": true
197 | }
198 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.js text eol=lf
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: testing-library
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_Report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: I think something is broken 😡
4 | ---
5 |
6 |
14 |
15 | - `react-native` or `expo`:
16 | - `native-testing-library` version:
17 | - `jest-preset`:
18 | - `react-native` version:
19 | - `node` version:
20 |
21 | ### Relevant code or config:
22 |
23 | ```js
24 | const your = code => 'here';
25 | ```
26 |
27 | ### What you did:
28 |
29 |
30 |
31 | ### What happened:
32 |
33 |
34 |
35 | ### Reproduction:
36 |
37 |
38 |
39 | ### Problem description:
40 |
41 |
42 |
43 | ### Suggested solution:
44 |
45 |
46 |
47 | ### Can you help us fix this issue by submitting a pull request?
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_Request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 💡 Feature Request
3 | about: I think something could be better 🤔
4 | ---
5 |
6 |
15 |
16 | **Describe the feature you'd like:**
17 |
18 |
19 |
20 | **Suggested implementation:**
21 |
22 |
23 |
24 | **Describe alternatives you've considered:**
25 |
26 |
27 |
28 | **Teachability, Documentation, Adoption, Migration Strategy:**
29 |
30 |
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ❓ Support Question
3 | about: I think I don't understand how to do something 🤨
4 | ---
5 |
6 |
7 |
8 | Issues on GitHub are intended to be related to problems and feature requests so we ask you not use
9 | issues to ask for support.
10 |
11 | Remember that this library is almost entirely the same as `react-testing-library` so you're likely
12 | to get some great advice by searching for how to do something with it.
13 |
14 | ---
15 |
16 | ## ❓ React Testing Library Resources
17 |
18 | - Discord https://discord.gg/c6JN9fM
19 | - Stack Overflow https://stackoverflow.com/questions/tagged/react-testing-library
20 |
21 | ## ❓ Native Testing Library Resources
22 |
23 | - Stack Overflow https://stackoverflow.com/questions/tagged/native-testing-library
24 |
25 | **ISSUES WHICH ARE QUESTIONS WILL BE CLOSED**
26 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | **What**:
12 |
13 |
14 |
15 | **Why**:
16 |
17 |
18 |
19 | **How**:
20 |
21 |
22 |
23 | **Checklist**:
24 |
25 |
26 |
27 |
28 |
29 | - [ ] Documentation added to the
30 | [docs site](https://github.com/bcarroll22/native-testing-library-docs)
31 | - [ ] Typescript definitions updated
32 | - [ ] Tests
33 | - [ ] Ready to be merged
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | dist/
4 | .idea/
5 | .DS_Store
6 |
7 | yarn-error.log
8 |
9 | package-lock.json
10 | yarn.lock
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=http://registry.npmjs.org/
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '8'
5 | - '9'
6 | - '10'
7 | - '11'
8 | cache:
9 | npm: true
10 | directories:
11 | - coverage
12 | notifications:
13 | email: false
14 | install: npm i && npm i -g codecov
15 | script: npm run test:coverage && codecov
16 | jobs:
17 | include:
18 | - stage: release
19 | if: branch = master AND type != pull_request
20 | node_js: '10'
21 | script: npm run semantic-release
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The changelog is automatically updated using
4 | [semantic-release](https://github.com/semantic-release/semantic-release). You
5 | can see it on the [releases page](../../releases).
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at brandonvcarroll@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2019 Brandon Carroll
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚠️ Deprecation notice ⚠️
2 |
3 | This repository has been deprecated in favor of https://github.com/callstack/react-native-testing-library and the `@testing-library/react-native` npm package will from now on (since v7.0) will be sourced from there. Please consult the [migration guide](https://callstack.github.io/react-native-testing-library/docs/migration-v7/#guide-for-testing-libraryreact-native-users).
4 |
5 |
6 |
7 |
Native Testing Library
8 |
9 |
10 |
16 |
17 |
18 |
Simple and complete React Native testing utilities that encourage good testing practices.
19 |
20 | [**Read The Docs**](https://native-testing-library.com/docs/intro) |
21 | [Edit the docs](https://github.com/testing-library/native-testing-library-docs)
22 |
23 |
24 |
25 |
26 | [](https://travis-ci.org/testing-library/native-testing-library)
27 | [](https://codecov.io/github/testing-library/native-testing-library)
28 | [](https://www.npmjs.com/package/@testing-library/react-native)
29 | [](http://www.npmtrends.com/@testing-library/react-native)
30 | [](https://github.com/testing-library/native-testing-library/blob/master/LICENSE)
31 |
32 | [](#contributors)
33 | [](http://makeapullrequest.com)
34 | [](https://github.com/testing-library/native-testing-library/blob/master/CODE_OF_CONDUCT.md)
35 | [](https://discord.gg/c6JN9fM)
36 |
37 | [](https://github.com/testing-library/native-testing-library/watchers)
38 | [](https://github.com/testing-library/native-testing-library/stargazers)
39 |
40 |
41 |
42 |
43 | ## Table of Contents
44 |
45 | - [The problem](#the-problem)
46 | - [This solution](#this-solution)
47 | - [Example](#example)
48 | - [Installation](#installation)
49 | - [Hooks](#hooks)
50 | - [Other Solutions](#other-solutions)
51 | - [Guiding Principles](#guiding-principles)
52 | - [Inspiration](#inspiration)
53 | - [Contributors](#contributors)
54 | - [Docs](#docs)
55 |
56 |
57 |
58 | ## The problem
59 |
60 | You want to write maintainable tests for your React Native application. You love Kent Dodds' testing
61 | library, and you want to be able to write maintainable tests for your React Native application. You
62 | don't want to use a library that renders components to a fake DOM, and you've had a hard time
63 | finding what you need to write tests using that philosophy in React Native.
64 |
65 | ## This solution
66 |
67 | `native-testing-library` is an implementation of the well-known testing-library API that works for
68 | React Native. The primary goal is to mimic the testing library API as closely as possible while
69 | still accounting for the differences in the platforms.
70 |
71 | ## Example
72 |
73 | ```javascript
74 | import React from 'react';
75 | import { Button, Text, TextInput, View } from 'react-native';
76 | import { fireEvent, render, wait } from '@testing-library/react-native';
77 |
78 | function Example() {
79 | const [name, setUser] = React.useState('');
80 | const [show, setShow] = React.useState(false);
81 |
82 | return (
83 |
84 |
85 | {
88 | // let's pretend this is making a server request, so it's async
89 | // (you'd want to mock this imaginary request in your unit tests)...
90 | setTimeout(() => {
91 | setShow(!show);
92 | }, Math.floor(Math.random() * 200));
93 | }}
94 | />
95 | {show && {name} }
96 |
97 | );
98 | }
99 |
100 | test('examples of some things', async () => {
101 | const { getByTestId, getByText, queryByTestId, baseElement } = render( );
102 | const famousWomanInHistory = 'Ada Lovelace';
103 |
104 | const input = getByTestId('input');
105 | fireEvent.changeText(input, famousWomanInHistory);
106 |
107 | const button = getByText('Print Username');
108 | fireEvent.press(button);
109 |
110 | await wait(() => expect(queryByTestId('printed-username')).toBeTruthy());
111 |
112 | expect(getByTestId('printed-username').props.children).toBe(famousWomanInHistory);
113 | expect(baseElement).toMatchSnapshot();
114 | });
115 | ```
116 |
117 | ## Installation
118 |
119 | This module should be installed in your project's `devDependencies`:
120 |
121 | ```
122 | npm install --save-dev @testing-library/react-native
123 | ```
124 |
125 | You will need `react` and `react-native` installed as _dependencies_ in order to run this project.
126 |
127 | ## Hooks
128 |
129 | If you are interested in testing a custom hook, check out
130 | [react-hooks-testing-library](https://github.com/mpeyper/react-hooks-testing-library).
131 |
132 | ## Other Solutions
133 |
134 | - [`react-native-testing-library`](https://github.com/callstack/react-native-testing-library)
135 | - [`enzyme`](https://airbnb.io/enzyme/docs/guides/react-native.html)
136 |
137 | ## Guiding principles
138 |
139 | > [The more your tests resemble the way your software is used, the more confidence they can give you.](https://twitter.com/kentcdodds/status/977018512689455106)
140 |
141 | We try to only expose methods and utilities that encourage you to write tests that closely resemble
142 | how your apps are used.
143 |
144 | Utilities are included in this project based on the following guiding principles:
145 |
146 | 1. If it relates to rendering components, it deals with native views rather than component
147 | instances, nor should it encourage dealing with component instances.
148 | 2. It should be generally useful for testing the application components in the way the user would
149 | use it. We are making some trade-offs here because we're using a computer and often a simulated
150 | environment, but in general, utilities should encourage tests that use the components the way
151 | they're intended to be used.
152 | 3. Utility implementations and APIs should be simple and flexible.
153 |
154 | In summary, we believe in the principles of `testing-library`, and adhere to them as closely as
155 | possible. At the end of the day, what we want is for this library to be pretty light-weight, simple,
156 | and understandable.
157 |
158 | ## Inspiration
159 |
160 | Huge thanks to Kent C. Dodds for evangelizing this approach to testing. We could have never come up
161 | with this library without him 🙏. Check out his awesome work and learn more about testing with
162 | confidence at [testingjavascript.com](https://testingjavascript.com/) (you won't regret purchasing
163 | it), and of course, use this library's big brother, `react-testing-library` for your DOM
164 | applications as well!
165 |
166 | The hook testing ability of this library is the same implementation as
167 | [react-hooks-testing-library](https://github.com/mpeyper/react-hooks-testing-library). The only
168 | reason it was included in this package is because we need you to import render from us, not the
169 | `dom-testing-library`, and that's an important blocker. Some day, maybe we'll try to allow use of
170 | that library with this one somehow.
171 |
172 | ## Contributors
173 |
174 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
175 |
176 |
177 |
178 |
179 |
206 |
207 |
208 |
209 |
210 |
211 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
212 | specification. Contributions of any kind welcome!
213 |
214 | ## Docs
215 |
216 | [**Read The Docs**](https://native-testing-library.com) |
217 | [Edit the docs](https://github.com/testing-library/native-testing-library-docs)
218 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | overrides: [
4 | {
5 | compact: false,
6 | },
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/cleanup-after-each.js:
--------------------------------------------------------------------------------
1 | afterEach(() => {
2 | return require('./dist/cleanup-async')();
3 | });
4 |
--------------------------------------------------------------------------------
/examples/__tests__/input-event.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TextInput } from 'react-native';
3 | import { render, fireEvent } from '../../src';
4 |
5 | class CostInput extends React.Component {
6 | state = {
7 | value: '',
8 | };
9 |
10 | removeDollarSign = value => (value[0] === '$' ? value.slice(1) : value);
11 | getReturnValue = value => (value === '' ? '' : `$${value}`);
12 | handleChange = ({ nativeEvent }) => {
13 | const text = nativeEvent.text;
14 | const noDollarSign = this.removeDollarSign(text);
15 | if (isNaN(noDollarSign)) return;
16 | this.setState({ value: this.getReturnValue(noDollarSign) });
17 | };
18 |
19 | render() {
20 | return (
21 |
26 | );
27 | }
28 | }
29 |
30 | const setup = () => {
31 | const utils = render( );
32 | const input = utils.getByLabelText('cost-input');
33 | return {
34 | input,
35 | ...utils,
36 | };
37 | };
38 |
39 | test('It should keep a $ in front of the input', () => {
40 | const { input } = setup();
41 |
42 | fireEvent.change(input, { nativeEvent: { text: 23 } });
43 | expect(input.props.value).toBe('$23');
44 | });
45 | test('It should allow a $ to be in the input when the value is changed', () => {
46 | const { input } = setup();
47 | fireEvent.change(input, { nativeEvent: { text: '$23.0' } });
48 | expect(input.props.value).toBe('$23.0');
49 | });
50 |
51 | test('It should not allow letters to be inputted', () => {
52 | const { input } = setup();
53 | expect(input.props.value).toBe('');
54 | fireEvent.change(input, { nativeEvent: { text: 'Good Day' } });
55 | expect(input.props.value).toBe('');
56 | });
57 |
58 | test('It should allow the $ to be deleted', () => {
59 | const { input } = setup();
60 | fireEvent.change(input, { nativeEvent: { text: '23' } });
61 | expect(input.props.value).toBe('$23');
62 | fireEvent.change(input, { nativeEvent: { text: '' } });
63 | expect(input.props.value).toBe('');
64 | });
65 |
--------------------------------------------------------------------------------
/examples/__tests__/react-context.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-native/extend-expect';
2 | import React from 'react';
3 | import { Text } from 'react-native';
4 | import { render } from '../../src';
5 |
6 | import { NameContext, NameProvider, NameConsumer } from '../react-context';
7 |
8 | test('NameConsumer shows default value', () => {
9 | const { getByText } = render( );
10 | expect(getByText(/^My Name Is:/)).toHaveTextContent('My Name Is: Unknown');
11 | });
12 |
13 | test('NameConsumer shows value from provider', () => {
14 | const tree = (
15 |
16 |
17 |
18 | );
19 | const { getByText } = render(tree);
20 | expect(getByText(/^My Name Is:/)).toHaveTextContent('My Name Is: C3P0');
21 | });
22 |
23 | test('NameProvider composes full name from first, last', () => {
24 | const tree = (
25 |
26 | {value => Received: {value} }
27 |
28 | );
29 | const { getByText } = render(tree);
30 | expect(getByText(/^Received:/)).toHaveTextContent('Received: Boba Fett');
31 | });
32 |
33 | test('NameProvider/Consumer shows name of character', () => {
34 | const tree = (
35 |
36 |
37 |
38 | );
39 | const { getByText } = render(tree);
40 | expect(getByText(/^My Name Is:/)).toHaveTextContent('My Name Is: Leia Organa');
41 | });
42 |
--------------------------------------------------------------------------------
/examples/__tests__/react-intl.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import { IntlProvider } from 'react-intl';
4 | import { FormattedDate } from 'react-intl-native';
5 | import IntlPolyfill from 'intl';
6 | import 'intl/locale-data/jsonp/pt';
7 |
8 | import { getByText, render } from '../../src';
9 |
10 | const setupTests = () => {
11 | if (global.Intl) {
12 | Intl.NumberFormat = IntlPolyfill.NumberFormat;
13 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
14 | } else {
15 | global.Intl = require('intl');
16 | }
17 | };
18 |
19 | const FormatDateView = () => {
20 | return (
21 |
22 |
29 |
30 | );
31 | };
32 |
33 | const renderWithReactIntl = component => {
34 | return {
35 | ...render({component} ),
36 | };
37 | };
38 |
39 | setupTests();
40 |
41 | test('it should render FormattedDate and have a formatted pt date', () => {
42 | const { container } = renderWithReactIntl( );
43 |
44 | getByText(container, '11/03/2019');
45 | });
46 |
--------------------------------------------------------------------------------
/examples/__tests__/react-navigation.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-native/extend-expect';
2 | import React from 'react';
3 | import { Button, Text, View } from 'react-native';
4 | import { createStackNavigator } from 'react-navigation-stack';
5 | import { createAppContainer, withNavigation } from 'react-navigation';
6 |
7 | import { render, fireEvent, cleanup } from '../../src';
8 |
9 | jest
10 | .mock('react-native/Libraries/Animated/src/NativeAnimatedHelper')
11 | .mock('react-native-gesture-handler', () => {
12 | const View = require('react-native').View;
13 | return {
14 | State: {},
15 | PanGestureHandler: View,
16 | BaseButton: View,
17 | Directions: {},
18 | };
19 | });
20 |
21 | const Home = ({ navigation }) => (
22 |
23 | Home page
24 | navigation.navigate('About')} />
25 |
26 | );
27 | const About = ({ navigation }) => (
28 |
29 | About page
30 | navigation.navigate('Home')} />
31 |
32 | );
33 | const Location = () => (
34 |
35 | Location page
36 |
37 |
38 | );
39 |
40 | const LocationDisplay = withNavigation(({ navigation }) => (
41 | {navigation.state.routeName}
42 | ));
43 |
44 | function renderWithNavigation({ screens = {}, navigatorConfig = {} } = {}) {
45 | const AppNavigator = createStackNavigator(
46 | {
47 | Home,
48 | About,
49 | Location,
50 | ...screens,
51 | },
52 | { initialRouteName: 'Home', ...navigatorConfig },
53 | );
54 |
55 | const App = createAppContainer(AppNavigator);
56 |
57 | return { ...render( ), navigationTestRenderer: App };
58 | }
59 |
60 | afterEach(cleanup);
61 |
62 | test('full app rendering/navigating', async () => {
63 | const { findByText, getByTestId, getByTitle } = renderWithNavigation();
64 |
65 | expect(getByTestId('title')).toHaveTextContent('Home page');
66 | fireEvent.press(getByTitle(/Go to about/i));
67 |
68 | const result = await findByText('About page');
69 | expect(result).toHaveTextContent('About page');
70 | });
71 |
72 | test('rendering a component that uses withNavigation', () => {
73 | const initialRouteName = 'Location';
74 | const { getByTestId } = renderWithNavigation({
75 | navigatorConfig: { initialRouteName },
76 | });
77 | expect(getByTestId('location-display')).toHaveTextContent(initialRouteName);
78 | });
79 |
--------------------------------------------------------------------------------
/examples/__tests__/react-redux.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-native/extend-expect';
2 | import React from 'react';
3 | import { createStore } from 'redux';
4 | import { Provider, connect } from 'react-redux';
5 | import { Button, Text, View } from 'react-native';
6 | import { render, fireEvent } from '../../src';
7 |
8 | class Counter extends React.Component {
9 | increment = () => {
10 | this.props.dispatch({ type: 'INCREMENT' });
11 | };
12 |
13 | decrement = () => {
14 | this.props.dispatch({ type: 'DECREMENT' });
15 | };
16 |
17 | render() {
18 | return (
19 |
20 | Counter
21 |
22 |
23 | {this.props.count}
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | const ConnectedCounter = connect(state => ({ count: state.count }))(Counter);
32 |
33 | function reducer(state = { count: 0 }, action) {
34 | switch (action.type) {
35 | case 'INCREMENT':
36 | return {
37 | count: state.count + 1,
38 | };
39 | case 'DECREMENT':
40 | return {
41 | count: state.count - 1,
42 | };
43 | default:
44 | return state;
45 | }
46 | }
47 |
48 | function renderWithRedux(ui, { initialState, store = createStore(reducer, initialState) } = {}) {
49 | return {
50 | ...render({ui} ),
51 | store,
52 | };
53 | }
54 |
55 | test('can render with redux with defaults', () => {
56 | const { getByTestId, getByTitle } = renderWithRedux( );
57 | fireEvent.press(getByTitle('+'));
58 | expect(getByTestId('count-value')).toHaveTextContent(1);
59 | });
60 |
61 | test('can render with redux with custom initial state', () => {
62 | const { getByTestId, getByTitle } = renderWithRedux( , {
63 | initialState: { count: 3 },
64 | });
65 | fireEvent.press(getByTitle('-'));
66 | expect(getByTestId('count-value')).toHaveTextContent(2);
67 | });
68 |
69 | test('can render with redux with custom store', () => {
70 | const store = createStore(() => ({ count: 1000 }));
71 | const { getByTestId, getByTitle } = renderWithRedux( , {
72 | store,
73 | });
74 | fireEvent.press(getByTitle('+'));
75 | expect(getByTestId('count-value')).toHaveTextContent(1000);
76 | fireEvent.press(getByTitle('-'));
77 | expect(getByTestId('count-value')).toHaveTextContent(1000);
78 | });
79 |
--------------------------------------------------------------------------------
/examples/__tests__/update-props.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { Text, View } from 'react-native';
4 | import { render } from '../../src';
5 |
6 | let idCounter = 1;
7 |
8 | class NumberDisplay extends React.Component {
9 | id = idCounter++;
10 | render() {
11 | return (
12 |
13 | {this.props.number}
14 | {this.id}
15 |
16 | );
17 | }
18 | }
19 |
20 | test('calling render with the same component on the same testRenderer does not remount', () => {
21 | const { getByTestId, rerender } = render( );
22 | expect(getByTestId('number-display')).toHaveTextContent(1);
23 |
24 | rerender( );
25 | expect(getByTestId('number-display')).toHaveTextContent(2);
26 |
27 | expect(getByTestId('instance-id')).toHaveTextContent(1);
28 | });
29 |
--------------------------------------------------------------------------------
/examples/__tests__/use-hooks.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { renderHook, act } from 'react-hooks-testing-library';
3 |
4 | describe('useState tests', () => {
5 | test('should use setState value', () => {
6 | const { result } = renderHook(() => useState('foo'));
7 | const [value] = result.current;
8 |
9 | expect(value).toBe('foo');
10 | });
11 |
12 | test('should update setState value using setter', () => {
13 | const { result } = renderHook(() => useState('foo'));
14 | const [_, setValue] = result.current;
15 |
16 | act(() => setValue('bar'));
17 |
18 | const [value] = result.current;
19 | expect(value).toBe('bar');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/examples/react-context.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 |
4 | const NameContext = React.createContext('Unknown');
5 |
6 | const NameProvider = ({ children, first, last }) => {
7 | const fullName = `${first} ${last}`;
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | const NameConsumer = () => (
16 | {value => My Name Is: {value} }
17 | );
18 |
19 | export { NameContext, NameConsumer, NameProvider };
20 |
--------------------------------------------------------------------------------
/jest-preset.js:
--------------------------------------------------------------------------------
1 | const jestPreset = require('react-native/jest-preset');
2 |
3 | module.exports = Object.assign(jestPreset, {
4 | transformIgnorePatterns: [
5 | ...jestPreset.transformIgnorePatterns,
6 | 'node_modules/(?!(react-native.*|@?react-navigation.*)/)',
7 | ],
8 | snapshotSerializers: [require.resolve('./dist/preset/serializer.js')],
9 | setupFiles: [...jestPreset.setupFiles, require.resolve('./dist/preset/setup.js')],
10 | });
11 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const jestPreset = require('react-native/jest-preset');
2 |
3 | const ignores = ['/node_modules/', '/__tests__/helpers/', '__mocks__'];
4 |
5 | module.exports = Object.assign(jestPreset, {
6 | collectCoverageFrom: ['**/src/lib/**/*.js', '!**/src/preset/**/*.js'],
7 | snapshotSerializers: [require.resolve('./src/preset/serializer.js')],
8 | setupFiles: [...jestPreset.setupFiles, require.resolve('./src/preset/setup.js')],
9 | testPathIgnorePatterns: [...ignores],
10 | transformIgnorePatterns: ['node_modules/(?!(react-native.*|@?react-navigation.*)/)'],
11 | coverageThreshold: {
12 | global: {
13 | branches: 100,
14 | functions: 100,
15 | lines: 100,
16 | statements: 100,
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/other/cheat-sheet.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/native-testing-library/c864cd573cd3ac9f40da8540d6ffbc6b8dc6aa52/other/cheat-sheet.pdf
--------------------------------------------------------------------------------
/other/whale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/native-testing-library/c864cd573cd3ac9f40da8540d6ffbc6b8dc6aa52/other/whale.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@testing-library/react-native",
3 | "version": "0.0.0-semantically-released",
4 | "description": "Simple and complete React Native testing utilities that encourage good testing practices.",
5 | "main": "dist/index.js",
6 | "typings": "typings/index.d.ts",
7 | "files": [
8 | "dist",
9 | "typings",
10 | "cleanup-after-each.js",
11 | "pure.js",
12 | "jest-preset.js"
13 | ],
14 | "engines": {
15 | "node": ">=8"
16 | },
17 | "scripts": {
18 | "commit": "git-cz",
19 | "commit:add": "git add .",
20 | "commit:all": "npm run commit:add && npm run commit",
21 | "readme:toc": "doctoc README.md --maxlevel 3 --title '## Table of Contents'",
22 | "test": "jest",
23 | "pretty-quick": "pretty-quick --staged",
24 | "prepublishOnly": "rm -rf dist; babel src --out-dir dist --ignore 'src/**/__tests__/*'",
25 | "semantic-release": "semantic-release",
26 | "test:coverage": "jest --coverage",
27 | "test:watch": "jest --watch --coverage"
28 | },
29 | "keywords": [
30 | "testing",
31 | "react",
32 | "react-native",
33 | "unit",
34 | "integration",
35 | "functional",
36 | "end-to-end",
37 | "e2e"
38 | ],
39 | "author": "Brandon Carroll (https://github.com/bcarroll22)",
40 | "license": "MIT",
41 | "dependencies": {
42 | "pretty-format": "^24.9.0",
43 | "wait-for-expect": "^3.0.0"
44 | },
45 | "devDependencies": {
46 | "@babel/cli": "7.2.3",
47 | "@babel/core": "7.8.4",
48 | "@babel/runtime": "7.4.0",
49 | "@testing-library/jest-native": "^3.1.0",
50 | "commitizen": "^3.0.7",
51 | "cz-conventional-changelog": "^2.1.0",
52 | "husky": "^1.3.1",
53 | "intl": "^1.2.5",
54 | "jest": "25.1.0",
55 | "jest-fetch-mock": "^2.1.1",
56 | "jest-in-case": "^1.0.2",
57 | "metro-react-native-babel-preset": "^0.59.0",
58 | "prettier": "^1.16.4",
59 | "pretty-quick": "^1.10.0",
60 | "react": "16.13.1",
61 | "react-hooks-testing-library": "^0.5.0",
62 | "react-intl": "^2.8.0",
63 | "react-intl-native": "^2.1.2",
64 | "react-native": "^0.63.0",
65 | "react-native-gesture-handler": "^1.1.0",
66 | "react-native-screens": "^2.9.0",
67 | "react-navigation": "^4.4.0",
68 | "react-navigation-stack": "^1.7.3",
69 | "react-redux": "^7.0.3",
70 | "react-test-renderer": "16.13.1",
71 | "redux": "^4.0.1",
72 | "semantic-release": "^15.13.3",
73 | "snapshot-diff": "0.5.1"
74 | },
75 | "peerDependencies": {
76 | "react": "*",
77 | "react-native": "*",
78 | "react-test-renderer": "*"
79 | },
80 | "husky": {
81 | "hooks": {
82 | "pre-commit": "npm run pretty-quick"
83 | }
84 | },
85 | "prettier": {
86 | "printWidth": 100,
87 | "singleQuote": true,
88 | "trailingComma": "all",
89 | "tabWidth": 2,
90 | "proseWrap": "always"
91 | },
92 | "release": {
93 | "branch": "master",
94 | "plugins": [
95 | "@semantic-release/commit-analyzer",
96 | "@semantic-release/release-notes-generator",
97 | "@semantic-release/npm",
98 | "@semantic-release/github"
99 | ]
100 | },
101 | "config": {
102 | "commitizen": {
103 | "path": "./node_modules/cz-conventional-changelog"
104 | }
105 | },
106 | "repository": {
107 | "type": "git",
108 | "url": "https://github.com/testing-library/native-testing-library.git"
109 | },
110 | "bugs": {
111 | "url": "https://github.com/testing-library/native-testing-library/issues"
112 | },
113 | "homepage": "https://github.com/testing-library/native-testing-library#readme"
114 | }
115 |
--------------------------------------------------------------------------------
/pure.js:
--------------------------------------------------------------------------------
1 | // makes it so people can import from '@testing-library/react-native/pure'
2 | module.exports = require('./dist/pure');
3 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/fetch.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Fetch makes an API call and displays the greeting when load-greeting is clicked 1`] = `
4 |
13 |
14 |
15 |
16 | Fetch
17 |
18 |
19 |
20 | hello there
21 |
22 |
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/misc.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`fragments can show diffs 1`] = `
4 | "Snapshot Diff:
5 | - First value
6 | + Second value
7 |
8 | @@ -6,8 +6,8 @@
9 | \\"flex\\": 1,
10 | }
11 | }
12 | >
13 |
17 | "
18 | `;
19 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/render.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`supports fragments 1`] = `
4 |
13 |
14 |
15 | Fragments are pretty cool!
16 |
17 |
18 |
19 | `;
20 |
--------------------------------------------------------------------------------
/src/__tests__/act.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { Button } from 'react-native';
4 |
5 | import { render, fireEvent, cleanup } from '../';
6 |
7 | afterEach(cleanup);
8 |
9 | test('render calls useEffect immediately', () => {
10 | const effectCb = jest.fn();
11 | function MyUselessComponent() {
12 | React.useEffect(effectCb);
13 | return null;
14 | }
15 | render( );
16 | expect(effectCb).toHaveBeenCalledTimes(1);
17 | });
18 |
19 | test('fireEvent triggers useEffect calls', () => {
20 | const effectCb = jest.fn();
21 |
22 | function Counter() {
23 | React.useEffect(effectCb);
24 | const [count, setCount] = React.useState(0);
25 | return setCount(count + 1)} title={`${count}`} />;
26 | }
27 | const { getByTitle } = render( );
28 | const buttonNode = getByTitle('0');
29 | effectCb.mockClear();
30 | fireEvent.press(buttonNode);
31 | expect(buttonNode.props.title).toBe('1');
32 | expect(effectCb).toHaveBeenCalledTimes(1);
33 | });
34 |
35 | test('calls to hydrate will run useEffects', () => {
36 | const effectCb = jest.fn();
37 | function MyUselessComponent() {
38 | React.useEffect(effectCb);
39 | return null;
40 | }
41 | render( , { hydrate: true });
42 | expect(effectCb).toHaveBeenCalledTimes(1);
43 | });
44 |
--------------------------------------------------------------------------------
/src/__tests__/bugs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { render, queryAllByProp, cleanup } from '../';
5 |
6 | afterEach(cleanup);
7 |
8 | // This is to ensure custom queries can be passed to render. In most cases, you
9 | // wouldn't/shouldn't need to do this, but we do allow it so we'll test to
10 | // make sure that it works for those who use it.
11 | test('returns the queries passed as options bound to the container', () => {
12 | const queryAllBySelectionColor = queryAllByProp.bind(null, 'selectionColor');
13 | const queries = { queryAllBySelectionColor };
14 |
15 | const { queryAllBySelectionColor: queryAllByImplementationDetail } = render(
16 |
17 | hello world
18 | ,
19 | { queries },
20 | );
21 |
22 | expect(queryAllByImplementationDetail('blue')).toHaveLength(1);
23 | });
24 |
--------------------------------------------------------------------------------
/src/__tests__/debug.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { cleanup, render } from '../';
5 |
6 | let log;
7 |
8 | beforeEach(() => {
9 | jest.spyOn(console, 'log').mockImplementation(output => (log = output));
10 | });
11 |
12 | afterEach(() => {
13 | cleanup();
14 | log = undefined;
15 | console.log.mockRestore();
16 | });
17 |
18 | test('debug pretty prints the baseElement', () => {
19 | const HelloWorld = () => Hello World ;
20 | const { debug } = render( );
21 | debug();
22 | expect(console.log).toHaveBeenCalledTimes(1);
23 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Hello World'));
24 | });
25 |
26 | test('debug can remove specified props from output', () => {
27 | const { debug } = render(
28 |
29 | Hello World!
30 | ,
31 | {
32 | options: {
33 | debug: {
34 | omitProps: ['style', 'pointerEvents', 'collapsable'],
35 | },
36 | },
37 | },
38 | );
39 |
40 | debug();
41 |
42 | expect(console.log).toHaveBeenCalledTimes(1);
43 | expect(log).toMatchInlineSnapshot(`
44 | "[36m[39m
45 | [36m[39m
46 | [36m[39m
47 | [36m[39m
48 | [0mHello World![0m
49 | [36m [39m
50 | [36m [39m
51 | [36m [39m
52 | [36m [39m"
53 | `);
54 | });
55 |
--------------------------------------------------------------------------------
/src/__tests__/end-to-end.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { Text } from 'react-native';
4 |
5 | import { cleanup, render, wait } from '../';
6 |
7 | afterEach(cleanup);
8 |
9 | const fetchAMessage = () =>
10 | new Promise(resolve => {
11 | // we are using random timeout here to simulate a real-time example
12 | // of an async operation calling a callback at a non-deterministic time
13 | const randomTimeout = Math.floor(Math.random() * 100);
14 | setTimeout(() => {
15 | resolve({ returnedMessage: 'Hello World' });
16 | }, randomTimeout);
17 | });
18 |
19 | class ComponentWithLoader extends React.Component {
20 | state = { loading: true };
21 | async componentDidMount() {
22 | const data = await fetchAMessage();
23 | this.setState({ data, loading: false });
24 | }
25 | render() {
26 | if (this.state.loading) {
27 | return Loading... ;
28 | }
29 | return Loaded this message: {this.state.data.returnedMessage}! ;
30 | }
31 | }
32 |
33 | test('it waits for the data to be loaded', async () => {
34 | const { queryByText, queryByTestId } = render( );
35 |
36 | expect(queryByText('Loading...')).toBeTruthy();
37 |
38 | await wait(() => expect(queryByText('Loading...')).toBeNull());
39 | expect(queryByTestId('message')).toHaveTextContent(/Hello World/);
40 | });
41 |
--------------------------------------------------------------------------------
/src/__tests__/events.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { Button, Image, Text, TextInput, TouchableHighlight } from 'react-native';
4 |
5 | import { render, fireEvent, eventMap, getEventHandlerName, wait, cleanup } from '../';
6 |
7 | afterEach(cleanup);
8 |
9 | Object.keys(eventMap).forEach(key => {
10 | describe(`${key} events`, () => {
11 | const config = eventMap[key];
12 |
13 | config.forEach(event => {
14 | const spy = jest.fn();
15 | const handlerName = getEventHandlerName(event);
16 |
17 | const {
18 | container: {
19 | children: [target],
20 | },
21 | } = render(
22 | React.createElement(key, {
23 | [handlerName]: spy,
24 | }),
25 | );
26 |
27 | fireEvent[event](target);
28 |
29 | expect(spy).toHaveBeenCalledTimes(1);
30 | });
31 | });
32 | });
33 |
34 | test('onChange works', () => {
35 | const handleChange = jest.fn();
36 | const {
37 | container: {
38 | children: [input],
39 | },
40 | } = render( );
41 |
42 | fireEvent.change(input, { target: { value: 'a' } });
43 | expect(handleChange).toHaveBeenCalledTimes(1);
44 | });
45 |
46 | test('onChangeText works', () => {
47 | function OnChangeText() {
48 | const [text, setText] = React.useState('first');
49 |
50 | return ;
51 | }
52 |
53 | const { getByTestId } = render( );
54 | const input = getByTestId('input');
55 |
56 | expect(input.props.value).toBe('first');
57 | fireEvent.changeText(input, 'second');
58 | expect(input.props.value).toBe('second');
59 | });
60 |
61 | test('assigns target properties', async () => {
62 | class MyComponent extends React.Component {
63 | state = { value: '' };
64 | onChange = ({ nativeEvent }) => {
65 | this.setState({ value: nativeEvent.text });
66 | this.props.onChange();
67 | };
68 | render() {
69 | return ;
70 | }
71 | }
72 |
73 | const spy = jest.fn();
74 | const value = 'a';
75 | const { getByTestId } = render( );
76 | const input = getByTestId('input');
77 | fireEvent.change(input, { nativeEvent: { text: value } });
78 | expect(spy).toHaveBeenCalledTimes(1);
79 | await wait(() => expect(input.props.value).toBe(value));
80 | });
81 |
82 | test('calling `fireEvent` directly works too', () => {
83 | const handleEvent = jest.fn();
84 | const { container } = render( );
85 |
86 | fireEvent(container.children[0], new NativeTestEvent('press'));
87 | expect(handleEvent).toBeCalledTimes(1);
88 | });
89 |
90 | test('calling a custom event works as well', () => {
91 | const event = { nativeEvent: { value: 'testing' } };
92 | const onMyEvent = jest.fn(({ nativeEvent }) => expect(nativeEvent).toEqual({ value: 'testing' }));
93 | const MyComponent = ({ onMyEvent }) => ;
94 |
95 | const {
96 | container: {
97 | children: [input],
98 | },
99 | } = render( );
100 |
101 | fireEvent(input, new NativeTestEvent('myEvent', event));
102 |
103 | expect(onMyEvent).toHaveBeenCalledWith({ nativeEvent: { value: 'testing' } });
104 | });
105 |
106 | test('calling a handler when there is no valid target does not work', () => {
107 | const handleEvent = jest.fn();
108 | const { getByTestId } = render( );
109 | expect(() => fireEvent.press(getByTestId('image'))).not.toThrow();
110 | expect(handleEvent).toBeCalledTimes(0);
111 | });
112 |
113 | test('calling a handler if a Button is disabled does not work', () => {
114 | const handleEvent = jest.fn();
115 | const { getByText } = render( );
116 | expect(() => fireEvent.press(getByText('button'))).not.toThrow();
117 | expect(handleEvent).toBeCalledTimes(0);
118 | });
119 |
120 | test('calling a handler if a Touchable is disabled does not work', () => {
121 | const handleEvent = jest.fn();
122 | const { getByText } = render(
123 |
124 | touchable
125 | ,
126 | );
127 | fireEvent.press(getByText('touchable'));
128 | expect(handleEvent).toBeCalledTimes(0);
129 | });
130 |
131 | test('calling an event that has no defined handler throws', () => {
132 | const { getByText } = render(test );
133 | const text = getByText('test');
134 | expect(() => fireEvent.changeText(text).toThrow());
135 | });
136 |
137 | test('calling an event sets nativeEvent properly', () => {
138 | const event = { nativeEvent: { value: 'testing' } };
139 | const onChange = jest.fn(({ nativeEvent }) => expect(nativeEvent).toEqual({ value: 'testing' }));
140 |
141 | const { getByDisplayValue } = render( );
142 | fireEvent.change(getByDisplayValue('test'), event);
143 | });
144 |
--------------------------------------------------------------------------------
/src/__tests__/fetch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { TouchableOpacity, Text, View } from 'react-native';
4 |
5 | import { render, fireEvent, wait, cleanup } from '../';
6 |
7 | afterEach(cleanup);
8 |
9 | global.fetch = require('jest-fetch-mock');
10 |
11 | class Fetch extends React.Component {
12 | state = {};
13 | componentDidUpdate(prevProps) {
14 | if (this.props.url !== prevProps.url) {
15 | this.fetch();
16 | }
17 | }
18 | fetch = async () => {
19 | const response = await fetch(this.props.url);
20 | const json = await response.json();
21 | this.setState({ data: json.data });
22 | };
23 | render() {
24 | const { data } = this.state;
25 | return (
26 |
27 |
28 | Fetch
29 |
30 | {data ? {data.greeting} : null}
31 |
32 | );
33 | }
34 | }
35 |
36 | test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
37 | fetch.mockResponseOnce(JSON.stringify({ data: { greeting: 'hello there' } }));
38 | const url = '/greeting';
39 | const { container, getByText } = render( );
40 |
41 | fireEvent.press(getByText('Fetch'));
42 |
43 | await wait();
44 |
45 | expect(fetch).toHaveBeenCalledTimes(1);
46 | expect(fetch).toHaveBeenCalledWith(url);
47 |
48 | expect(getByText('hello there')).toHaveTextContent('hello there');
49 | expect(container).toMatchSnapshot();
50 | });
51 |
--------------------------------------------------------------------------------
/src/__tests__/forms.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, TextInput, View } from 'react-native';
3 | import { render, fireEvent, cleanup } from '../';
4 |
5 | afterEach(cleanup);
6 |
7 | function Login({ onSubmit, user }) {
8 | return (
9 |
10 |
16 |
22 | onSubmit(user)} />
23 |
24 | );
25 | }
26 |
27 | test('login form submits', () => {
28 | const fakeUser = { username: 'bcarroll', password: 'starboy' };
29 | const handleSubmit = jest.fn();
30 | const { getByTitle } = render( );
31 |
32 | const submitButtonNode = getByTitle('Submit');
33 |
34 | fireEvent.press(submitButtonNode);
35 |
36 | expect(handleSubmit).toHaveBeenCalledTimes(1);
37 | expect(handleSubmit).toHaveBeenCalledWith(fakeUser);
38 | });
39 |
--------------------------------------------------------------------------------
/src/__tests__/misc.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import {
3 | Button,
4 | Picker,
5 | Pressable,
6 | ScrollView,
7 | Switch,
8 | Text,
9 | TextInput,
10 | TouchableHighlight,
11 | TouchableNativeFeedback,
12 | TouchableOpacity,
13 | TouchableWithoutFeedback,
14 | View,
15 | } from 'react-native';
16 | import { toMatchDiffSnapshot } from 'snapshot-diff';
17 |
18 | import { cleanup, fireEvent, render } from '../';
19 |
20 | afterEach(cleanup);
21 |
22 | test(' works', () => {
23 | const fireZeMissiles = jest.fn();
24 |
25 | function Wrapper() {
26 | return (
27 |
28 | missiles
29 |
30 | );
31 | }
32 | const { getByText } = render( );
33 |
34 | fireEvent.press(getByText('missiles'));
35 | expect(fireZeMissiles).toBeCalledTimes(1);
36 | });
37 |
38 | test(' works', () => {
39 | function Wrapper() {
40 | const [value, setValue] = React.useState('js');
41 |
42 | return (
43 | setValue(itemValue)}>
44 |
45 |
46 |
47 | );
48 | }
49 | const { findByDisplayValue, getByDisplayValue } = render( );
50 |
51 | fireEvent.valueChange(getByDisplayValue('js'), 'java');
52 | expect(() => findByDisplayValue('js')).not.toThrow();
53 | });
54 |
55 | test(' instance methods are mocked', () => {
56 | function Wrapper() {
57 | const ref = useRef();
58 |
59 | useEffect(() => {
60 | ref.current.scrollTo(0);
61 | }, []);
62 |
63 | return (
64 |
65 | Some content
66 |
67 | );
68 | }
69 | const { getByText, debug } = render( );
70 |
71 | expect(() => getByText('Some content')).not.toThrow();
72 | });
73 |
74 | test(' instance methods are mocked', () => {
75 | function Wrapper() {
76 | const ref = useRef();
77 |
78 | useEffect(() => {
79 | ref.current.clear();
80 | }, []);
81 |
82 | return ;
83 | }
84 | const { getByDisplayValue } = render( );
85 |
86 | expect(() => getByDisplayValue('yo')).not.toThrow();
87 | });
88 |
89 | test('calling a handler if a Touchable is disabled does not work', () => {
90 | const handleEvent = jest.fn();
91 | const { getByText } = render(
92 |
93 | touchable
94 | ,
95 | );
96 | expect(() => fireEvent.press(getByText('touchable'))).not.toThrow();
97 | expect(handleEvent).toBeCalledTimes(0);
98 | });
99 |
100 | test('calling a TouchableHighlight handler works', () => {
101 | const handleEvent = jest.fn();
102 | const { getByText } = render(
103 |
104 | touchable
105 | ,
106 | );
107 | expect(() => fireEvent.press(getByText('touchable'))).not.toThrow();
108 | expect(handleEvent).toBeCalledTimes(1);
109 | });
110 |
111 | test('calling a TouchableNativeFeedback handler works', () => {
112 | const handleEvent = jest.fn();
113 | const { getByText } = render(
114 |
115 | touchable
116 | ,
117 | );
118 | expect(() => fireEvent.press(getByText('touchable'))).not.toThrow();
119 | expect(handleEvent).toBeCalledTimes(1);
120 | });
121 |
122 | test('calling a TouchableOpacity handler works', () => {
123 | const handleEvent = jest.fn();
124 | const { getByText } = render(
125 |
126 | touchable
127 | ,
128 | );
129 | expect(() => fireEvent.press(getByText('touchable'))).not.toThrow();
130 | expect(handleEvent).toBeCalledTimes(1);
131 | });
132 |
133 | test('calling a TouchableWithoutFeedback handler works ', () => {
134 | const handleEvent = jest.fn();
135 | const { getByText } = render(
136 |
137 | touchable
138 | ,
139 | );
140 | expect(() => fireEvent.press(getByText('touchable'))).not.toThrow();
141 | expect(handleEvent).toBeCalledTimes(1);
142 | });
143 |
144 | test('fragments can show diffs', () => {
145 | function TestComponent() {
146 | const [count, setCount] = React.useState(0);
147 |
148 | return (
149 | setCount(count => count + 1)} title={`Click to increase: ${count}`} />
150 | );
151 | }
152 |
153 | expect.extend({ toMatchDiffSnapshot });
154 |
155 | const { getByText, asJSON } = render( );
156 | const firstRender = asJSON();
157 |
158 | fireEvent.press(getByText(/Click to increase/));
159 |
160 | // This will snapshot only the difference between the first render, and the
161 | // state of the DOM after the click event.
162 | // See https://github.com/jest-community/snapshot-diff
163 | expect(firstRender).toMatchDiffSnapshot(asJSON());
164 | });
165 |
166 | test('finds only valid children', () => {
167 | const Wrapper = ({ children }) => {children} ;
168 |
169 | const { container } = render(
170 |
171 |
172 | hey
173 | sup
174 |
175 |
176 | ,
177 | );
178 |
179 | expect(
180 | // AppContainer
181 | // => node text
182 | // => Text
183 | // => View (from Wrapper)
184 | // => View
185 | container.children[0].children[0].children[0].children[0],
186 | ).toBe('hey');
187 | });
188 |
189 | test('it finds only valid parents', () => {
190 | const Wrapper = ({ children }) => {children} ;
191 |
192 | const { baseElement, getByText } = render(
193 |
194 |
195 | hey
196 | sup
197 |
198 | ,
199 | );
200 |
201 | expect(getByText('hey').parentNode.parentNode.props.testID).toBe('view');
202 | expect(baseElement.parentNode).toBeNull();
203 | });
204 |
205 | test('it finds by value={false}', () => {
206 | const { getByDisplayValue } = render( );
207 |
208 | getByDisplayValue(false);
209 | });
210 |
211 | test("it finds by value={'hey'} when another a Switch with value={true} is present", () => {
212 | const { getByDisplayValue } = render(
213 |
214 |
215 |
216 | ,
217 | );
218 |
219 | getByDisplayValue('hey');
220 | });
221 |
222 | test("it finds by value={'java'} when another a Switch with value={true} is present", () => {
223 | const { getByDisplayValue } = render(
224 |
225 |
226 | setValue(itemValue)}>
227 |
228 |
229 |
230 | ,
231 | );
232 |
233 | getByDisplayValue('java');
234 | });
235 |
--------------------------------------------------------------------------------
/src/__tests__/new-act.js:
--------------------------------------------------------------------------------
1 | let asyncAct;
2 |
3 | jest.mock('react-test-renderer', () => ({
4 | act: cb => {
5 | return cb();
6 | },
7 | }));
8 |
9 | beforeEach(() => {
10 | jest.resetModules();
11 | asyncAct = require('../act-compat').asyncAct;
12 | jest.spyOn(console, 'error').mockImplementation(() => {});
13 | });
14 |
15 | afterEach(() => {
16 | console.error.mockRestore();
17 | });
18 |
19 | test('async act works when it does not exist (older versions of react)', async () => {
20 | const callback = jest.fn();
21 | await asyncAct(async () => {
22 | await Promise.resolve();
23 | await callback();
24 | });
25 | expect(console.error).toHaveBeenCalledTimes(0);
26 | expect(callback).toHaveBeenCalledTimes(1);
27 |
28 | callback.mockClear();
29 | console.error.mockClear();
30 |
31 | await asyncAct(async () => {
32 | await Promise.resolve();
33 | await callback();
34 | });
35 | expect(console.error).toHaveBeenCalledTimes(0);
36 | expect(callback).toHaveBeenCalledTimes(1);
37 | });
38 |
39 | test('async act recovers from errors', async () => {
40 | try {
41 | await asyncAct(async () => {
42 | await null;
43 | throw new Error('test error');
44 | });
45 | } catch (err) {
46 | console.error('call console.error');
47 | }
48 | expect(console.error).toHaveBeenCalledTimes(1);
49 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
50 | Array [
51 | Array [
52 | "call console.error",
53 | ],
54 | ]
55 | `);
56 | });
57 |
58 | test('async act recovers from sync errors', async () => {
59 | try {
60 | await asyncAct(() => {
61 | throw new Error('test error');
62 | });
63 | } catch (err) {
64 | console.error('call console.error');
65 | }
66 | expect(console.error).toHaveBeenCalledTimes(1);
67 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
68 | Array [
69 | Array [
70 | "call console.error",
71 | ],
72 | ]
73 | `);
74 | });
75 |
76 | /* eslint no-console:0 */
77 |
--------------------------------------------------------------------------------
/src/__tests__/no-act.js:
--------------------------------------------------------------------------------
1 | let act, asyncAct;
2 |
3 | beforeEach(() => {
4 | jest.resetModules();
5 | act = require('..').act;
6 | asyncAct = require('../act-compat').asyncAct;
7 | jest.spyOn(console, 'error').mockImplementation(() => {});
8 | });
9 |
10 | afterEach(() => {
11 | console.error.mockRestore();
12 | });
13 |
14 | jest.mock('react-test-renderer', () => ({}));
15 |
16 | test('act works even when there is no act from test renderer', () => {
17 | const callback = jest.fn();
18 | act(callback);
19 | expect(callback).toHaveBeenCalledTimes(1);
20 | expect(console.error).toHaveBeenCalledTimes(0);
21 | });
22 |
23 | test('async act works when it does not exist (older versions of react)', async () => {
24 | const callback = jest.fn();
25 | await asyncAct(async () => {
26 | await Promise.resolve();
27 | await callback();
28 | });
29 | expect(console.error).toHaveBeenCalledTimes(0);
30 | expect(callback).toHaveBeenCalledTimes(1);
31 |
32 | callback.mockClear();
33 | console.error.mockClear();
34 |
35 | await asyncAct(async () => {
36 | await Promise.resolve();
37 | await callback();
38 | });
39 | expect(console.error).toHaveBeenCalledTimes(0);
40 | expect(callback).toHaveBeenCalledTimes(1);
41 | });
42 |
43 | test('async act recovers from errors', async () => {
44 | try {
45 | await asyncAct(async () => {
46 | await null;
47 | throw new Error('test error');
48 | });
49 | } catch (err) {
50 | console.error('call console.error');
51 | }
52 | expect(console.error).toHaveBeenCalledTimes(1);
53 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
54 | Array [
55 | Array [
56 | "call console.error",
57 | ],
58 | ]
59 | `);
60 | });
61 |
62 | test('async act recovers from sync errors', async () => {
63 | try {
64 | await asyncAct(() => {
65 | throw new Error('test error');
66 | });
67 | } catch (err) {
68 | console.error('call console.error');
69 | }
70 | expect(console.error).toHaveBeenCalledTimes(1);
71 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
72 | Array [
73 | Array [
74 | "call console.error",
75 | ],
76 | ]
77 | `);
78 | });
79 |
80 | /* eslint no-console:0 */
81 |
--------------------------------------------------------------------------------
/src/__tests__/old-act.js:
--------------------------------------------------------------------------------
1 | let asyncAct;
2 |
3 | beforeEach(() => {
4 | jest.resetModules();
5 | asyncAct = require('../act-compat').asyncAct;
6 | jest.spyOn(console, 'error').mockImplementation(() => {});
7 | });
8 |
9 | afterEach(() => {
10 | console.error.mockRestore();
11 | });
12 |
13 | jest.mock('react-test-renderer', () => ({
14 | act: cb => {
15 | cb();
16 | return {
17 | then() {
18 | console.error(
19 | 'Warning: Do not await the result of calling TestRenderer.act(...), it is not a Promise.',
20 | );
21 | },
22 | };
23 | },
24 | }));
25 |
26 | test('async act works even when the act is an old one', async () => {
27 | const callback = jest.fn();
28 | await asyncAct(async () => {
29 | console.error('sigil');
30 | await Promise.resolve();
31 | await callback();
32 | console.error('sigil');
33 | });
34 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
35 | Array [
36 | Array [
37 | Array [
38 | "sigil",
39 | ],
40 | ],
41 | Array [
42 | "It looks like you're using a version of react-test-renderer that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-test-renderer@16.9.0 to remove this warning.",
43 | ],
44 | Array [
45 | "sigil",
46 | ],
47 | ]
48 | `);
49 | expect(callback).toHaveBeenCalledTimes(1);
50 |
51 | // and it doesn't warn you twice
52 | callback.mockClear();
53 | console.error.mockClear();
54 |
55 | await asyncAct(async () => {
56 | await Promise.resolve();
57 | await callback();
58 | });
59 | expect(console.error).toHaveBeenCalledTimes(0);
60 | expect(callback).toHaveBeenCalledTimes(1);
61 | });
62 |
63 | test('async act recovers from async errors', async () => {
64 | try {
65 | await asyncAct(async () => {
66 | await null;
67 | throw new Error('test error');
68 | });
69 | } catch (err) {
70 | console.error('call console.error');
71 | }
72 | expect(console.error).toHaveBeenCalledTimes(2);
73 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
74 | Array [
75 | Array [
76 | "It looks like you're using a version of react-test-renderer that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-test-renderer@16.9.0 to remove this warning.",
77 | ],
78 | Array [
79 | "call console.error",
80 | ],
81 | ]
82 | `);
83 | });
84 |
85 | test('async act recovers from sync errors', async () => {
86 | try {
87 | await asyncAct(() => {
88 | throw new Error('test error');
89 | });
90 | } catch (err) {
91 | console.error('call console.error');
92 | }
93 | expect(console.error).toHaveBeenCalledTimes(1);
94 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
95 | Array [
96 | Array [
97 | "call console.error",
98 | ],
99 | ]
100 | `);
101 | });
102 |
103 | test('async act can handle any sort of console.error', async () => {
104 | await asyncAct(async () => {
105 | console.error({ error: 'some error' });
106 | await null;
107 | });
108 |
109 | expect(console.error).toHaveBeenCalledTimes(2);
110 | expect(console.error.mock.calls).toMatchInlineSnapshot(`
111 | Array [
112 | Array [
113 | Array [
114 | Object {
115 | "error": "some error",
116 | },
117 | ],
118 | ],
119 | Array [
120 | "It looks like you're using a version of react-test-renderer that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-test-renderer@16.9.0 to remove this warning.",
121 | ],
122 | ]
123 | `);
124 | });
125 |
126 | test('async act should not show an error when ReactTestUtils.act returns something', async () => {
127 | jest.resetModules();
128 | jest.mock('react-test-renderer', () => ({
129 | act: () => {
130 | return new Promise(resolve => {
131 | console.error(
132 | 'Warning: The callback passed to TestRenderer.act(...) function must not return anything',
133 | );
134 | resolve();
135 | });
136 | },
137 | }));
138 | asyncAct = require('../act-compat').asyncAct;
139 | await asyncAct(async () => {
140 | await null;
141 | });
142 |
143 | expect(console.error).toHaveBeenCalledTimes(0);
144 | });
145 |
146 | /* eslint no-console:0 */
147 |
--------------------------------------------------------------------------------
/src/__tests__/render.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, SafeAreaView, View } from 'react-native';
3 |
4 | import { cleanup, render } from '../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('renders View', () => {
9 | const { container } = render( );
10 | expect(container).not.toBeNull();
11 | });
12 |
13 | test('returns container', () => {
14 | const { container } = render( );
15 | expect(container).toBeTruthy();
16 | });
17 |
18 | it('supports fragments', () => {
19 | class Test extends React.Component {
20 | render() {
21 | return (
22 |
23 | Fragments are pretty cool!
24 |
25 | );
26 | }
27 | }
28 |
29 | const { asJSON, unmount } = render( );
30 | expect(asJSON()).toMatchSnapshot();
31 | unmount();
32 | expect(asJSON()).toBeNull();
33 | });
34 |
35 | test('renders options.wrapper around node', () => {
36 | const WrapperComponent = ({ children }) => (
37 | {children}
38 | );
39 |
40 | const { container, getByTestId } = render( , {
41 | wrapper: WrapperComponent,
42 | });
43 |
44 | expect(getByTestId('wrapper')).toBeTruthy();
45 | expect(container).toMatchInlineSnapshot(`
46 |
55 |
58 |
61 |
62 |
63 | `);
64 | });
65 |
--------------------------------------------------------------------------------
/src/__tests__/rerender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-native/extend-expect';
3 | import { Text } from 'react-native';
4 |
5 | import { cleanup, render } from '../';
6 |
7 | afterEach(cleanup);
8 |
9 | test('rerender will re-render the element', () => {
10 | const Greeting = props => {props.message} ;
11 | const { getByText, rerender } = render( );
12 |
13 | const message = getByText('hi');
14 |
15 | expect(message).toHaveTextContent('hi');
16 | rerender( );
17 | expect(message).toHaveTextContent('hey');
18 | });
19 |
--------------------------------------------------------------------------------
/src/__tests__/stopwatch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Text, View } from 'react-native';
3 |
4 | import { render, fireEvent, prettyPrint, cleanup } from '../';
5 |
6 | afterEach(cleanup);
7 |
8 | class StopWatch extends React.Component {
9 | state = { lapse: 0, running: false };
10 | handleRunClick = () => {
11 | this.setState(state => {
12 | if (state.running) {
13 | clearInterval(this.timer);
14 | } else {
15 | const startTime = Date.now() - this.state.lapse;
16 | this.timer = setInterval(() => {
17 | this.setState({ lapse: Date.now() - startTime });
18 | });
19 | }
20 | return { running: !state.running };
21 | });
22 | };
23 | handleClearClick = () => {
24 | clearInterval(this.timer);
25 | this.setState({ lapse: 0, running: false });
26 | };
27 | componentWillUnmount() {
28 | clearInterval(this.timer);
29 | }
30 | render() {
31 | const { lapse, running } = this.state;
32 | return (
33 |
34 | {lapse}ms
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | const wait = time => new Promise(resolve => setTimeout(resolve, time));
43 |
44 | test('unmounts a component', async () => {
45 | jest.spyOn(console, 'error').mockImplementation(() => {});
46 | const { unmount, getByTitle, container } = render( );
47 | fireEvent.press(getByTitle('Start'));
48 |
49 | unmount();
50 | // hey there reader! You don't need to have an assertion like this one
51 | // this is just me making sure that the unmount function works.
52 | // You don't need to do this in your apps. Just rely on the fact that this works.
53 | expect(prettyPrint(container)).toBe('null');
54 | await wait(() => expect(console.error).not.toHaveBeenCalled());
55 | });
56 |
--------------------------------------------------------------------------------
/src/act-compat.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as testUtils from 'react-test-renderer';
3 |
4 | const reactAct = testUtils.act;
5 | const actSupported = reactAct !== undefined;
6 |
7 | function actPolyfill(cb) {
8 | cb();
9 | }
10 |
11 | const act = reactAct || actPolyfill;
12 |
13 | let youHaveBeenWarned = false;
14 | let isAsyncActSupported = null;
15 |
16 | function asyncAct(cb) {
17 | if (actSupported === true) {
18 | if (isAsyncActSupported === null) {
19 | return new Promise((resolve, reject) => {
20 | // patch console.error here
21 | const originalConsoleError = console.error;
22 | console.error = function error(...args) {
23 | /* if console.error fired *with that specific message* */
24 | /* istanbul ignore next */
25 | const firstArgIsString = typeof args[0] === 'string';
26 | if (
27 | firstArgIsString &&
28 | args[0].indexOf('Warning: Do not await the result of calling TestRenderer.act') === 0
29 | ) {
30 | // v16.8.6
31 | isAsyncActSupported = false;
32 | } else if (
33 | firstArgIsString &&
34 | args[0].indexOf(
35 | 'Warning: The callback passed to TestRenderer.act(...) function must not return anything',
36 | ) === 0
37 | ) {
38 | // no-op
39 | } else {
40 | originalConsoleError.call(console, args);
41 | }
42 | };
43 | let cbReturn, result;
44 | try {
45 | result = reactAct(() => {
46 | cbReturn = cb();
47 | return cbReturn;
48 | });
49 | } catch (err) {
50 | console.error = originalConsoleError;
51 | reject(err);
52 | return;
53 | }
54 |
55 | result.then(
56 | () => {
57 | console.error = originalConsoleError;
58 | // if it got here, it means async act is supported
59 | isAsyncActSupported = true;
60 | resolve();
61 | },
62 | err => {
63 | console.error = originalConsoleError;
64 | isAsyncActSupported = true;
65 | reject(err);
66 | },
67 | );
68 |
69 | // 16.8.6's act().then() doesn't call a resolve handler, so we need to manually flush here, sigh
70 |
71 | if (isAsyncActSupported === false) {
72 | console.error = originalConsoleError;
73 | /* istanbul-ignore-next */
74 | if (!youHaveBeenWarned) {
75 | // if act is supported and async act isn't and they're trying to use async
76 | // act, then they need to upgrade from 16.8 to 16.9.
77 | // This is a seemless upgrade, so we'll add a warning
78 | console.error(
79 | `It looks like you're using a version of react-test-renderer that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-test-renderer@16.9.0 to remove this warning.`,
80 | );
81 | youHaveBeenWarned = true;
82 | }
83 |
84 | cbReturn.then(() => {
85 | // a faux-version.
86 | // todo - copy https://github.com/facebook/react/blob/master/packages/shared/enqueueTask.js
87 | Promise.resolve().then(() => {
88 | // use sync act to flush effects
89 | act(() => {});
90 | resolve();
91 | });
92 | }, reject);
93 | }
94 | });
95 | } else if (isAsyncActSupported === false) {
96 | // use the polyfill directly
97 | let result;
98 | act(() => {
99 | result = cb();
100 | });
101 | return result.then(() => {
102 | return Promise.resolve().then(() => {
103 | // use sync act to flush effects
104 | act(() => {});
105 | });
106 | });
107 | }
108 | // all good! regular act
109 | return act(cb);
110 | }
111 | // use the polyfill
112 | let result;
113 | act(() => {
114 | result = cb();
115 | });
116 | return result.then(() => {
117 | return Promise.resolve().then(() => {
118 | // use sync act to flush effects
119 | act(() => {});
120 | });
121 | });
122 | }
123 |
124 | export default act;
125 | export { asyncAct };
126 |
127 | /* eslint no-console:0 */
128 |
--------------------------------------------------------------------------------
/src/cleanup-async.js:
--------------------------------------------------------------------------------
1 | // This file is for use by the top-level export
2 | // @testing-library/react/cleanup-after-each
3 | // It is not meant to be used directly
4 |
5 | module.exports = async function cleanupAsync() {
6 | const { asyncAct } = require('./act-compat');
7 | const { cleanup } = require('./index');
8 | await asyncAct(async () => {});
9 | cleanup();
10 | };
11 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TR from 'react-test-renderer';
3 | import AppContainer from 'react-native/Libraries/ReactNative/AppContainer';
4 |
5 | import {
6 | toJSON,
7 | fireEvent as rntlFireEvent,
8 | getQueriesForElement,
9 | NativeTestEvent,
10 | prettyPrint,
11 | proxyElement,
12 | } from './lib';
13 | import act from './act-compat';
14 |
15 | const renderers = new Set();
16 |
17 | function render(ui, { options = {}, wrapper: WrapperComponent, queries } = {}) {
18 | const { debug, ...rest } = options;
19 |
20 | const wrapUiIfNeeded = innerElement =>
21 | WrapperComponent ? (
22 |
23 | {innerElement}
24 |
25 | ) : (
26 | {innerElement}
27 | );
28 |
29 | let testRenderer;
30 |
31 | act(() => {
32 | testRenderer = TR.create(wrapUiIfNeeded(ui), rest);
33 | });
34 |
35 | renderers.add(testRenderer);
36 |
37 | const wrappers = proxyElement(testRenderer.root).findAll(n => n.type === 'View');
38 | const baseElement = wrappers[0];
39 | const container = wrappers[1]; // Includes only your render
40 |
41 | return {
42 | baseElement,
43 | container,
44 | debug: (el = baseElement) => console.log(prettyPrint(el, undefined, { debug })),
45 | unmount: () => testRenderer.unmount(),
46 | rerender: rerenderUi => {
47 | act(() => {
48 | testRenderer.update(wrapUiIfNeeded(rerenderUi));
49 | });
50 | },
51 | asJSON: () => {
52 | return toJSON(container);
53 | },
54 | ...getQueriesForElement(baseElement, queries),
55 | };
56 | }
57 |
58 | function cleanup() {
59 | renderers.forEach(cleanupRenderer);
60 | }
61 |
62 | function cleanupRenderer(renderer) {
63 | renderer.unmount();
64 | renderers.delete(renderer);
65 | }
66 |
67 | function fireEvent(...args) {
68 | let returnValue;
69 | act(() => {
70 | returnValue = rntlFireEvent(...args);
71 | });
72 | return returnValue;
73 | }
74 |
75 | Object.keys(rntlFireEvent).forEach(typeArg => {
76 | fireEvent[typeArg] = (...args) => {
77 | let returnValue;
78 | act(() => {
79 | returnValue = rntlFireEvent[typeArg](...args);
80 | });
81 | return returnValue;
82 | };
83 | });
84 |
85 | export * from './lib';
86 | export { act, cleanup, fireEvent, render, NativeTestEvent };
87 |
--------------------------------------------------------------------------------
/src/lib/__tests__/__snapshots__/to-json.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`it converts to json 1`] = `
4 | "[36m[39m
13 | [36m[39m
14 | [36m[39m
15 | [36m[39m
16 | [36m[39m
17 | [0mhello[0m
18 | [36m [39m
19 | [36m [39m
20 | [36m[39m
21 | [36m[39m
22 | [0mworld[0m
23 | [36m [39m
24 | [36m[39m
25 | [0mfoo bar[0m
26 | [36m [39m
27 | [36m [39m
28 | [36m [39m
29 | [36m [39m
30 | [36m [39m
31 | [36m [39m"
32 | `;
33 |
34 | exports[`it converts to json 2`] = `
35 | "[36m[39m
43 | [36m[39m
52 | [36m[39m
53 | [36m[39m
54 | [36m[39m
55 | [36m[39m
56 | [0mhello[0m
57 | [36m [39m
58 | [36m [39m
59 | [36m[39m
60 | [36m[39m
61 | [0mworld[0m
62 | [36m [39m
63 | [36m[39m
64 | [0mfoo bar[0m
65 | [36m [39m
66 | [36m [39m
67 | [36m [39m
68 | [36m [39m
69 | [36m [39m
70 | [36m [39m
71 | [36m [39m"
72 | `;
73 |
--------------------------------------------------------------------------------
/src/lib/__tests__/config.js:
--------------------------------------------------------------------------------
1 | import { configure, getConfig } from '../config';
2 |
3 | describe('configuration API', () => {
4 | let originalConfig;
5 | beforeEach(() => {
6 | // Grab the existing configuration so we can restore
7 | // it at the end of the test
8 | configure(existingConfig => {
9 | originalConfig = existingConfig;
10 | // Don't change the existing config
11 | return {};
12 | });
13 | });
14 | afterEach(() => {
15 | configure(originalConfig);
16 | });
17 |
18 | beforeEach(() => {
19 | configure({ foo: '123', bar: '456' });
20 | });
21 |
22 | describe('getConfig', () => {
23 | test('returns existing configuration', () => {
24 | const conf = getConfig();
25 | expect(conf.foo).toEqual('123');
26 | });
27 | });
28 |
29 | describe('configure', () => {
30 | test('merges a delta rather than replacing the whole config', () => {
31 | const conf = getConfig();
32 | expect(conf).toMatchObject({ foo: '123', bar: '456' });
33 | });
34 |
35 | test('overrides existing values', () => {
36 | configure({ foo: '789' });
37 | const conf = getConfig();
38 | expect(conf.foo).toEqual('789');
39 | });
40 |
41 | test('passes existing config out to config function', () => {
42 | // Create a new config key based on the value of an existing one
43 | configure(existingConfig => ({
44 | foo: `${existingConfig.foo}-derived`,
45 | }));
46 | const conf = getConfig();
47 |
48 | // The new value should be there, and existing values should be
49 | // untouched
50 | expect(conf).toMatchObject({
51 | foo: '123-derived',
52 | });
53 | });
54 |
55 | test('asyncWrapper callback exists by default', () => {
56 | const callback = jest.fn();
57 | getConfig().asyncWrapper(callback);
58 | expect(callback).toHaveBeenCalledTimes(1);
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/lib/__tests__/get-by-errors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Text, TextInput, View } from 'react-native';
3 | import cases from 'jest-in-case';
4 |
5 | import { cleanup, render } from '../../';
6 |
7 | afterEach(cleanup);
8 |
9 | cases(
10 | 'getBy* queries throw an error when there are multiple elements returned',
11 | ({ name, query, tree }) => {
12 | const utils = render(tree);
13 | expect(() => utils[name](query)).toThrow(/multiple elements/i);
14 | },
15 | {
16 | getByHintText: {
17 | query: /his/,
18 | tree: (
19 |
20 |
21 |
22 |
23 | ),
24 | },
25 | getByLabelText: {
26 | query: /his/,
27 | tree: (
28 |
29 |
30 |
31 |
32 | ),
33 | },
34 | getByRole: {
35 | query: 'button',
36 | tree: (
37 |
38 |
39 |
40 |
41 | ),
42 | },
43 | getByPlaceholderText: {
44 | query: /his/,
45 | tree: (
46 |
47 |
48 |
49 |
50 | ),
51 | },
52 | getByTestId: {
53 | query: /his/,
54 | tree: (
55 |
56 | text
57 | other
58 |
59 | ),
60 | },
61 | getByText: {
62 | query: /his/,
63 | tree: (
64 |
65 | his
66 | history
67 |
68 | ),
69 | },
70 | getByTitle: {
71 | query: /his/,
72 | tree: (
73 |
74 |
75 |
76 |
77 | ),
78 | },
79 | getByDisplayValue: {
80 | query: /his/,
81 | tree: (
82 |
83 |
84 |
85 |
86 | ),
87 | },
88 | },
89 | );
90 |
91 | cases(
92 | 'queryBy* queries throw an error when there are multiple elements returned',
93 | ({ name, query, tree }) => {
94 | const utils = render(tree);
95 | expect(() => utils[name](query)).toThrow(/multiple elements/i);
96 | },
97 | {
98 | queryByHintText: {
99 | query: /his/,
100 | tree: (
101 |
102 |
103 |
104 |
105 | ),
106 | },
107 | queryByLabelText: {
108 | query: /his/,
109 | tree: (
110 |
111 |
112 |
113 |
114 | ),
115 | },
116 | queryByRole: {
117 | query: 'button',
118 | tree: (
119 |
120 |
121 |
122 |
123 | ),
124 | },
125 | queryByPlaceholderText: {
126 | query: /his/,
127 | tree: (
128 |
129 |
130 |
131 |
132 | ),
133 | },
134 | queryByTestId: {
135 | query: /his/,
136 | tree: (
137 |
138 | text
139 | other
140 |
141 | ),
142 | },
143 | queryByText: {
144 | query: /his/,
145 | tree: (
146 |
147 | his
148 | history
149 |
150 | ),
151 | },
152 | queryByTitle: {
153 | query: /his/,
154 | tree: (
155 |
156 |
157 |
158 |
159 | ),
160 | },
161 | queryByDisplayValue: {
162 | query: /his/,
163 | tree: (
164 |
165 |
166 |
167 |
168 | ),
169 | },
170 | },
171 | );
172 |
--------------------------------------------------------------------------------
/src/lib/__tests__/helpers.js:
--------------------------------------------------------------------------------
1 | import { getSetImmediate } from '../helpers';
2 |
3 | test('if setImmediate is available, use it', () => {
4 | const setImmediateMock = jest.fn();
5 | global.setImmediate = setImmediateMock;
6 |
7 | expect(getSetImmediate()).toBe(setImmediateMock);
8 | });
9 |
10 | test('if setImmediate is not available, use setTimeout', () => {
11 | const setImmediateMock = {};
12 | const setTimeoutMock = jest.fn();
13 | global.setImmediate = setImmediateMock;
14 | global.setTimeout = setTimeoutMock;
15 |
16 | const setImmediate = getSetImmediate();
17 |
18 | expect(setImmediate).not.toBe(setImmediateMock);
19 | setImmediate();
20 | expect(setTimeoutMock).toHaveBeenCalledTimes(1);
21 | });
22 |
--------------------------------------------------------------------------------
/src/lib/__tests__/matches.js:
--------------------------------------------------------------------------------
1 | import { fuzzyMatches, matches } from '../matches';
2 |
3 | const node = null;
4 | const normalizer = str => str;
5 |
6 | test('matchers accept strings', () => {
7 | expect(matches('ABC', node, 'ABC', normalizer)).toBe(true);
8 | expect(fuzzyMatches('ABC', node, 'ABC', normalizer)).toBe(true);
9 | });
10 |
11 | test('matchers accept regex', () => {
12 | expect(matches('ABC', node, /ABC/, normalizer)).toBe(true);
13 | expect(fuzzyMatches('ABC', node, /ABC/, normalizer)).toBe(true);
14 | });
15 |
16 | test('matchers accept functions', () => {
17 | expect(matches('ABC', node, text => text === 'ABC', normalizer)).toBe(true);
18 | expect(fuzzyMatches('ABC', node, text => text === 'ABC', normalizer)).toBe(true);
19 | });
20 |
21 | test('matchers return false if text to match is not a string', () => {
22 | expect(matches(null, node, 'ABC', normalizer)).toBe(false);
23 | expect(fuzzyMatches(null, node, 'ABC', normalizer)).toBe(false);
24 | });
25 |
26 | test('matchers return true if text to match is a boolean', () => {
27 | expect(matches(true, node, true, normalizer)).toBe(true);
28 | expect(fuzzyMatches(1, node, true, normalizer)).toBe(true);
29 | });
30 |
--------------------------------------------------------------------------------
/src/lib/__tests__/misc.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Picker, Switch, View, Text, TextInput, Button } from 'react-native';
3 |
4 | import { render, queryByProp, queryByTestId, cleanup } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('queryByProp', () => {
9 | const { container } = render(
10 |
11 |
12 |
13 |
14 | ,
15 | );
16 |
17 | expect(queryByTestId(container, 'foo')).not.toBeNull();
18 | expect(queryByProp('importantForAccessibility', container, 'auto')).toBeNull();
19 | expect(() => queryByProp('importantForAccessibility', container, /no/)).toThrow(
20 | /multiple elements/,
21 | );
22 | });
23 |
24 | it('should render test', () => {
25 | const { getByDisplayValue } = render(
26 |
27 |
28 |
29 |
30 |
31 | ,
32 | );
33 |
34 | expect(getByDisplayValue(true)).toBeTruthy();
35 | });
36 |
37 | test('selector option in queries filter out elements', () => {
38 | function filterByLabel(label) {
39 | return {
40 | selector: ({ props }) => props.accessibilityLabel === label,
41 | };
42 | }
43 |
44 | const { getByText, getByRole, getByDisplayValue, getByTitle } = render(
45 | <>
46 | hello world
47 | hello world
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | >,
58 | );
59 |
60 | // more than one match:
61 | expect(() => getByText(/hello world/i)).toThrow();
62 | // filtered
63 | getByText(/hello world/i, filterByLabel('labelled'));
64 |
65 | // more than one match:
66 | expect(() => getByRole('link')).toThrow();
67 | // filtered
68 | getByRole('link', filterByLabel('labelled'));
69 |
70 | // more than one match:
71 | expect(() => getByDisplayValue(/hello joe/i)).toThrow();
72 | // filtered
73 | getByDisplayValue(/hello joe/i, filterByLabel('labelled'));
74 |
75 | // more than one match:
76 | expect(() => getByTitle(/hello joe/i)).toThrow();
77 | // filtered
78 | getByTitle(/hello joe/i, filterByLabel('labelled'));
79 | });
80 |
--------------------------------------------------------------------------------
/src/lib/__tests__/pretty-print.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { render, prettyPrint, cleanup } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('it prints correctly with no children', () => {
9 | const { container } = render( );
10 |
11 | expect(prettyPrint(container)).toMatchInlineSnapshot(`
12 | "[36m[39m
21 | [36m [39m
22 | [36m [39m"
23 | `);
24 | });
25 |
26 | test('it prints correctly with one child', () => {
27 | const { container } = render(
28 |
29 | Hello World!
30 | ,
31 | );
32 |
33 | expect(prettyPrint(container)).toMatchInlineSnapshot(`
34 | "[36m[39m
43 | [36m[39m
44 | [36m[39m
45 | [0mHello World![0m
46 | [36m [39m
47 | [36m [39m
48 | [36m [39m"
49 | `);
50 | });
51 |
52 | test('it prints correctly with multiple children', () => {
53 | const { container } = render(
54 |
55 | Hello
56 | World!
57 | ,
58 | );
59 |
60 | expect(prettyPrint(container)).toMatchInlineSnapshot(`
61 | "[36m[39m
70 | [36m[39m
71 | [36m[39m
72 | [0mHello[0m
73 | [36m [39m
74 | [36m[39m
75 | [0mWorld![0m
76 | [36m [39m
77 | [36m [39m
78 | [36m [39m"
79 | `);
80 | });
81 |
82 | test('it supports truncating the output length', () => {
83 | const { container } = render(
84 |
85 | Hello World!
86 | ,
87 | );
88 |
89 | expect(prettyPrint(container, 5)).toMatch(/\.\.\./);
90 | });
91 |
92 | test('it supports removing props from output', () => {
93 | const { container } = render(
94 |
95 | Hello World!
96 | ,
97 | );
98 |
99 | expect(prettyPrint(container, undefined, { debug: { omitProps: ['style', 'pointerEvents'] } }))
100 | .toMatchInlineSnapshot(`
101 | "[36m[39m
104 | [36m[39m
105 | [36m[39m
106 | [0mHello World![0m
107 | [36m [39m
108 | [36m [39m
109 | [36m [39m"
110 | `);
111 | });
112 |
--------------------------------------------------------------------------------
/src/lib/__tests__/queries.find.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Image, Text, TextInput, View } from 'react-native';
3 |
4 | import { cleanup, render } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('find asynchronously finds elements', async () => {
9 | const {
10 | findAllByHintText,
11 | findAllByLabelText,
12 | findAllByRole,
13 | findAllByPlaceholderText,
14 | findAllByTestId,
15 | findAllByText,
16 | findAllByTitle,
17 | findAllByDisplayValue,
18 | findByHintText,
19 | findByLabelText,
20 | findByRole,
21 | findByPlaceholderText,
22 | findByTestId,
23 | findByText,
24 | findByTitle,
25 | findByDisplayValue,
26 | } = render(
27 |
28 |
29 |
30 |
31 | test text content
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ,
44 | );
45 |
46 | // Things get annoying querying accessibilityTraits with `queryByRole`
47 | jest.spyOn(console, 'warn').mockImplementation(() => {});
48 |
49 | await expect(findByHintText('test-hint')).resolves.toBeTruthy();
50 | await expect(findAllByHintText('test-hint')).resolves.toHaveLength(1);
51 |
52 | await expect(findByLabelText('test-label')).resolves.toBeTruthy();
53 | await expect(findAllByLabelText('test-label')).resolves.toHaveLength(1);
54 |
55 | await expect(findByPlaceholderText('placeholder')).resolves.toBeTruthy();
56 | await expect(findAllByPlaceholderText('placeholder')).resolves.toHaveLength(1);
57 |
58 | await expect(findByText('test text content')).resolves.toBeTruthy();
59 | await expect(findAllByText('test text content')).resolves.toHaveLength(1);
60 |
61 | await expect(findByTitle('button')).resolves.toBeTruthy();
62 | await expect(findAllByTitle('button')).resolves.toHaveLength(1);
63 |
64 | await expect(findByText('button')).resolves.toBeTruthy();
65 | await expect(findAllByText('button')).resolves.toBeTruthy();
66 |
67 | await expect(findByDisplayValue('value')).resolves.toBeTruthy();
68 | await expect(findAllByDisplayValue('value')).resolves.toHaveLength(1);
69 |
70 | await expect(findByRole('text')).resolves.toBeTruthy();
71 | await expect(findAllByRole('text')).resolves.toHaveLength(1);
72 |
73 | await expect(findByRole('button')).resolves.toBeTruthy();
74 | await expect(findAllByRole('button')).resolves.toHaveLength(1);
75 |
76 | await expect(findByRole(['button'])).resolves.toBeTruthy();
77 | await expect(findAllByRole(['button'])).resolves.toHaveLength(1);
78 |
79 | await expect(findByRole('none')).resolves.toBeTruthy();
80 | await expect(findAllByRole('none')).resolves.toHaveLength(1);
81 |
82 | await expect(findByRole('tablist')).resolves.toBeTruthy();
83 | await expect(findAllByRole('tablist')).resolves.toHaveLength(1);
84 |
85 | await expect(findByRole('tablist')).resolves.toBeTruthy();
86 | await expect(findAllByRole('tablist')).resolves.toHaveLength(1);
87 |
88 | await expect(findByRole('tab')).resolves.toBeTruthy();
89 | await expect(findAllByRole('tab')).resolves.toHaveLength(1);
90 |
91 | await expect(findByRole('fake', {}, { timeout: 50 })).rejects.toThrow();
92 |
93 | await expect(findByTestId('test-id')).resolves.toBeTruthy();
94 | await expect(findAllByTestId('test-id')).resolves.toHaveLength(1);
95 |
96 | console.warn.mock.calls.forEach(([message]) => {
97 | expect(message).toMatch(/Found elements matching accessibilityTraits/);
98 | });
99 | });
100 |
101 | test('find rejects when something cannot be found', async () => {
102 | const {
103 | findAllByHintText,
104 | findAllByLabelText,
105 | findAllByRole,
106 | findAllByPlaceholderText,
107 | findAllByTestId,
108 | findAllByText,
109 | findAllByTitle,
110 | findAllByDisplayValue,
111 | findByHintText,
112 | findByLabelText,
113 | findByRole,
114 | findByPlaceholderText,
115 | findByTestId,
116 | findByText,
117 | findByTitle,
118 | findByDisplayValue,
119 | } = render( );
120 |
121 | const qo = {};
122 | const wo = { timeout: 10 };
123 |
124 | await expect(findByHintText('x', qo, wo)).rejects.toThrow('x');
125 | await expect(findAllByHintText('x', qo, wo)).rejects.toThrow('x');
126 |
127 | await expect(findByLabelText('x', qo, wo)).rejects.toThrow('x');
128 | await expect(findAllByLabelText('x', qo, wo)).rejects.toThrow('x');
129 |
130 | await expect(findByRole('x', qo, wo)).rejects.toThrow('x');
131 | await expect(findAllByRole('x', qo, wo)).rejects.toThrow('x');
132 |
133 | await expect(findByPlaceholderText('x', qo, wo)).rejects.toThrow('x');
134 | await expect(findAllByPlaceholderText('x', qo, wo)).rejects.toThrow('x');
135 |
136 | await expect(findByText('x', qo, wo)).rejects.toThrow('x');
137 | await expect(findAllByText('x', qo, wo)).rejects.toThrow('x');
138 |
139 | await expect(findByTitle('x', qo, wo)).rejects.toThrow('x');
140 | await expect(findAllByTitle('x', qo, wo)).rejects.toThrow('x');
141 |
142 | await expect(findByDisplayValue('x', qo, wo)).rejects.toThrow('x');
143 | await expect(findAllByDisplayValue('x', qo, wo)).rejects.toThrow('x');
144 |
145 | await expect(findByTestId('x', qo, wo)).rejects.toThrow('x');
146 | await expect(findAllByTestId('x', qo, wo)).rejects.toThrow('x');
147 | });
148 |
149 | test('actually works with async code', async () => {
150 | const { findByTestId, rerender } = render( );
151 | setTimeout(() => rerender(correct tree ), 20);
152 | await expect(findByTestId('text', {})).resolves.toBeTruthy();
153 | });
154 |
--------------------------------------------------------------------------------
/src/lib/__tests__/query-helpers.js:
--------------------------------------------------------------------------------
1 | import { validComponentFilter, proxyElement } from '../query-helpers';
2 | import { configure } from '../config';
3 |
4 | describe('validComponentFilter > no key provided', () => {
5 | test('returns `true` when node.type is a string', () => {
6 | expect(validComponentFilter({ type: 'Text' })).toEqual(true);
7 | });
8 | test('validComponentFilter returns `false` when node.type is not in the mocked type list', () => {
9 | expect(validComponentFilter({ type: () => {} })).toEqual(false);
10 | });
11 | });
12 |
13 | describe('validComponentFilter > key provided', () => {
14 | test('validComponentFilter returns `true` when node.type is in the mocked type list', () => {
15 | configure({ testComponents: ['Text'] });
16 | expect(validComponentFilter({ type: 'Text' }, 'testComponents')).toEqual(true);
17 | });
18 | test('validComponentFilter returns `false` when node.type is not in the mocked type list', () => {
19 | configure({ testComponents: ['Text'] });
20 | expect(validComponentFilter({ type: 'Test' }, 'testComponents')).toEqual(false);
21 | });
22 | });
23 |
24 | test('proxyElement ignores what it should', () => {
25 | const testElement = proxyElement({
26 | _fiber: 'should work',
27 | find: jest.fn(),
28 | findAllByProps: jest.fn(),
29 | findAllByType: jest.fn(),
30 | findByProps: jest.fn(),
31 | findByType: jest.fn(),
32 | instance: jest.fn(),
33 | });
34 |
35 | expect(testElement._fiber).toBe('should work');
36 | expect(testElement.find).toBe(undefined);
37 | expect(testElement.findAllByProps).toBe(undefined);
38 | expect(testElement.findAllByType).toBe(undefined);
39 | expect(testElement.findByProps).toBe(undefined);
40 | expect(testElement.findByType).toBe(undefined);
41 | expect(testElement.instance).toBe(undefined);
42 | });
43 |
--------------------------------------------------------------------------------
/src/lib/__tests__/text-matchers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cases from 'jest-in-case';
3 | import { Button, Image, Text, TextInput, TouchableOpacity } from 'react-native';
4 |
5 | import { cleanup, getDefaultNormalizer, render } from '../../';
6 |
7 | afterEach(cleanup);
8 |
9 | cases(
10 | 'matches find case-sensitive full strings by default',
11 | ({ tree, query, queryFn }) => {
12 | const queries = render(tree);
13 |
14 | const queryString = query;
15 | const queryRegex = new RegExp(query);
16 | const queryFunc = text => text === query;
17 |
18 | expect(queries[queryFn](queryString)).toBeTruthy();
19 | expect(queries[queryFn](queryRegex)).toBeTruthy();
20 | expect(queries[queryFn](queryFunc)).toBeTruthy();
21 |
22 | expect(queries[queryFn](query.toUpperCase())).toBeFalsy();
23 | expect(queries[queryFn](query.slice(0, 1))).toBeFalsy();
24 | },
25 | {
26 | queryByTestId: {
27 | tree: (
28 |
29 | Link
30 |
31 | ),
32 | query: `link`,
33 | queryFn: `queryByTestId`,
34 | },
35 | queryByHintText: {
36 | tree: ,
37 | query: `Finding Nemo poster`,
38 | queryFn: `queryByHintText`,
39 | },
40 | queryByLabelText: {
41 | tree: ,
42 | query: `Finding Nemo poster`,
43 | queryFn: `queryByLabelText`,
44 | },
45 | queryByPlaceholderText: {
46 | tree: ,
47 | query: `Dwayne 'The Rock' Johnson`,
48 | queryFn: `queryByPlaceholderText`,
49 | },
50 | queryByText: {
51 | tree: Some content ,
52 | query: `Some content`,
53 | queryFn: `queryByText`,
54 | },
55 | queryByTitle: {
56 | tree: ,
57 | query: `link`,
58 | queryFn: `queryByTitle`,
59 | },
60 | },
61 | );
62 |
63 | cases(
64 | 'queries trim leading, trailing & inner whitespace by default',
65 | ({ tree, query, queryFn }) => {
66 | const queries = render(tree);
67 | expect(queries[queryFn](query)).toBeTruthy();
68 | expect(
69 | queries[queryFn](query, {
70 | normalizer: getDefaultNormalizer({
71 | collapseWhitespace: false,
72 | trim: false,
73 | }),
74 | }),
75 | ).toBeFalsy();
76 | },
77 | {
78 | queryByTestId: {
79 | tree: (
80 |
81 | Link
82 |
83 | ),
84 | query: `link`,
85 | queryFn: `queryByTestId`,
86 | },
87 | queryByHintText: {
88 | tree: (
89 |
94 | ),
95 | query: `Finding Nemo poster`,
96 | queryFn: `queryByHintText`,
97 | },
98 | queryByLabelText: {
99 | tree: (
100 |
105 | ),
106 | query: `Finding Nemo poster`,
107 | queryFn: `queryByLabelText`,
108 | },
109 | queryByPlaceholderText: {
110 | tree: ,
111 | query: `Dwayne 'The Rock' Johnson`,
112 | queryFn: `queryByPlaceholderText`,
113 | },
114 | queryByText: {
115 | tree: (
116 |
117 | {`
118 | Content
119 | with
120 | linebreaks
121 | is
122 | ok
123 | `}
124 |
125 | ),
126 | query: `Content with linebreaks is ok`,
127 | queryFn: `queryByText`,
128 | },
129 | queryByTitle: {
130 | tree: ,
131 | query: `link`,
132 | queryFn: `queryByTitle`,
133 | },
134 | },
135 | );
136 |
137 | cases(
138 | '{ exact } option toggles case-insensitive partial matches',
139 | ({ tree, query, queryFn }) => {
140 | const queries = render(tree);
141 |
142 | const queryString = query;
143 | const queryRegex = new RegExp(query);
144 | const queryFunc = text => text === query;
145 |
146 | expect(queries[queryFn](query)).toHaveLength(1);
147 |
148 | expect(queries[queryFn](queryString, { exact: false })).toHaveLength(1);
149 | expect(queries[queryFn](queryRegex, { exact: false })).toHaveLength(1);
150 | expect(queries[queryFn](queryFunc, { exact: false })).toHaveLength(1);
151 |
152 | expect(queries[queryFn](query.split(' ')[0], { exact: false })).toHaveLength(1);
153 | expect(queries[queryFn](query.toLowerCase(), { exact: false })).toHaveLength(1);
154 | },
155 | {
156 | queryAllByPlaceholderText: {
157 | tree: ,
158 | query: `Dwayne 'The Rock' Johnson`,
159 | queryFn: `queryAllByPlaceholderText`,
160 | },
161 | queryAllByDisplayValue: {
162 | tree: ,
163 | query: `Dwayne 'The Rock' Johnson`,
164 | queryFn: `queryAllByDisplayValue`,
165 | },
166 | queryAllByHintText: {
167 | tree: ,
168 | query: `Finding Nemo poster`,
169 | queryFn: `queryAllByHintText`,
170 | },
171 | queryAllByLabelText: {
172 | tree: ,
173 | query: `Finding Nemo poster`,
174 | queryFn: `queryAllByLabelText`,
175 | },
176 | queryAllByText: {
177 | tree: (
178 |
179 | {`
180 | Content
181 | with
182 | linebreaks
183 | is
184 | ok
185 | `}
186 |
187 | ),
188 | query: `Content with linebreaks is ok`,
189 | queryFn: `queryAllByText`,
190 | },
191 | queryAllByTitle: {
192 | tree: ,
193 | query: `link`,
194 | queryFn: `queryAllByTitle`,
195 | },
196 | },
197 | );
198 |
199 | const LRM = '\u200e';
200 | function removeUCC(str) {
201 | return str.replace(/[\u200e]/g, '');
202 | }
203 |
204 | cases(
205 | '{ normalizer } option allows custom pre-match normalization',
206 | ({ tree, queryFn }) => {
207 | const queries = render(tree);
208 |
209 | const query = queries[queryFn];
210 |
211 | expect(query(/user n.me/i, { normalizer: removeUCC })).toHaveLength(1);
212 | expect(query('User name', { normalizer: removeUCC })).toHaveLength(1);
213 |
214 | expect(query(/user n.me/i)).toHaveLength(0);
215 | expect(query('User name')).toHaveLength(0);
216 | },
217 | {
218 | queryAllByPlaceholderText: {
219 | tree: ,
220 | queryFn: 'queryAllByPlaceholderText',
221 | },
222 | queryAllByText: {
223 | tree: {`User ${LRM}name`} ,
224 | queryFn: 'queryAllByText',
225 | },
226 | queryAllByHintText: {
227 | tree: ,
228 | queryFn: 'queryAllByHintText',
229 | },
230 | queryAllByLabelText: {
231 | tree: ,
232 | queryFn: 'queryAllByLabelText',
233 | },
234 | queryAllByDisplayValue: {
235 | tree: ,
236 | queryFn: 'queryAllByDisplayValue',
237 | },
238 | },
239 | );
240 |
241 | test('normalizer works with both exact and non-exact matching', () => {
242 | const { queryAllByText } = render({`MiXeD ${LRM}CaSe`} );
243 |
244 | expect(queryAllByText('mixed case', { exact: false, normalizer: removeUCC })).toHaveLength(1);
245 | expect(queryAllByText('mixed case', { exact: true, normalizer: removeUCC })).toHaveLength(0);
246 | expect(queryAllByText('MiXeD CaSe', { exact: true, normalizer: removeUCC })).toHaveLength(1);
247 | expect(queryAllByText('MiXeD CaSe', { exact: true })).toHaveLength(0);
248 | });
249 |
250 | test('top-level trim and collapseWhitespace options are not supported if normalizer is specified', () => {
251 | const { queryAllByText } = render( abc def );
252 | const normalizer = str => str;
253 |
254 | expect(() => queryAllByText('abc', { trim: false, normalizer })).toThrow();
255 | expect(() => queryAllByText('abc', { trim: true, normalizer })).toThrow();
256 | expect(() => queryAllByText('abc', { collapseWhitespace: false, normalizer })).toThrow();
257 | expect(() => queryAllByText('abc', { collapseWhitespace: true, normalizer })).toThrow();
258 | });
259 |
260 | test('getDefaultNormalizer returns a normalizer that supports trim and collapseWhitespace', () => {
261 | expect(getDefaultNormalizer()(' abc def ')).toEqual('abc def');
262 | expect(getDefaultNormalizer({ trim: false })(' abc def ')).toEqual(' abc def ');
263 | expect(getDefaultNormalizer({ collapseWhitespace: false })(' abc def ')).toEqual('abc def');
264 | expect(getDefaultNormalizer({ trim: false, collapseWhitespace: false })(' abc def ')).toEqual(
265 | ' abc def ',
266 | );
267 | });
268 |
269 | test('we support an older API with trim and collapseWhitespace instead of a normalizer', () => {
270 | const { queryAllByText } = render({` x y `} );
271 | expect(queryAllByText('x y').length).toBe(1);
272 | expect(queryAllByText('x y', { trim: false }).length).toBe(0);
273 | expect(queryAllByText(' x y ', { trim: false }).length).toBe(1);
274 | expect(queryAllByText('x y', { collapseWhitespace: false }).length).toBe(0);
275 | expect(queryAllByText('x y', { collapseWhitespace: false }).length).toBe(1);
276 | });
277 |
--------------------------------------------------------------------------------
/src/lib/__tests__/to-json.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { cleanup, prettyPrint, render, toJSON } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('it converts to json', () => {
9 | function ParentComponent({ children }) {
10 | return {children} ;
11 | }
12 |
13 | function MiddleComponent({ children }) {
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | }
20 |
21 | const { baseElement, container } = render(
22 |
23 |
24 | hello
25 |
26 | world
27 | foo bar
28 |
29 |
30 |
31 | ,
32 | );
33 |
34 | expect(prettyPrint(container)).toMatchSnapshot();
35 | expect(prettyPrint(baseElement)).toMatchSnapshot();
36 | });
37 |
38 | test('it handles an no argument', () => {
39 | expect(toJSON()).toBeNull();
40 | });
41 |
42 | test('it handles hidden nodes', () => {
43 | expect(toJSON({ _fiber: { stateNode: { isHidden: true } } })).toBeNull();
44 | });
45 |
46 | test('it handles invalid nodes', () => {
47 | expect(() => toJSON({ _fiber: { stateNode: { tag: 'FAKE' } } })).toThrow();
48 | });
49 |
--------------------------------------------------------------------------------
/src/lib/__tests__/wait-for-element-to-be-removed.fake-timers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 |
4 | import { cleanup, render, waitForElementToBeRemoved } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | jest.useFakeTimers();
9 |
10 | test('requires a function as the first parameter', () => {
11 | return expect(waitForElementToBeRemoved()).rejects.toThrowErrorMatchingInlineSnapshot(
12 | `"waitForElementToBeRemoved requires a callback as the first parameter"`,
13 | );
14 | });
15 |
16 | test('requires an element to exist first', () => {
17 | return expect(waitForElementToBeRemoved(() => null)).rejects.toThrowErrorMatchingInlineSnapshot(
18 | `"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal."`,
19 | );
20 | });
21 |
22 | test('requires an unempty array of elements to exist first', () => {
23 | return expect(waitForElementToBeRemoved(() => [])).rejects.toThrowErrorMatchingInlineSnapshot(
24 | `"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal."`,
25 | );
26 | });
27 |
28 | test('times out after 4500ms by default', () => {
29 | const { container } = render( );
30 | const promise = expect(
31 | waitForElementToBeRemoved(() => container),
32 | ).rejects.toThrowErrorMatchingInlineSnapshot(`"Timed out in waitForElementToBeRemoved."`);
33 |
34 | jest.advanceTimersByTime(4501);
35 |
36 | return promise;
37 | });
38 |
--------------------------------------------------------------------------------
/src/lib/__tests__/wait-for-element-to-be-removed.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 |
4 | import { cleanup, render, waitForElementToBeRemoved } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('resolves only when the element is removed', async () => {
9 | class MutatedElement extends React.Component {
10 | state = {
11 | text: 'original',
12 | visible: true,
13 | };
14 |
15 | componentDidMount() {
16 | // mutation
17 | this.setState({ text: 'mutated' });
18 |
19 | // removal
20 | setTimeout(() => {
21 | this.setState({ visible: false });
22 | }, 100);
23 | }
24 |
25 | render() {
26 | return this.state.visible ? {this.state.text} : null;
27 | }
28 | }
29 |
30 | const { queryAllByTestId } = render( );
31 |
32 | // the timeout is here for two reasons:
33 | // 1. It helps test the timeout config
34 | // 2. The element should be removed immediately
35 | // so if it doesn't in the first 100ms then we know something's wrong
36 | // so we'll fail early and not wait the full timeout
37 | await waitForElementToBeRemoved(() => queryAllByTestId('view'), { timeout: 250 });
38 | });
39 |
40 | test('resolves on mutation if callback throws an error', async () => {
41 | class MutatedElement extends React.Component {
42 | state = {
43 | visible: true,
44 | };
45 |
46 | componentDidMount() {
47 | setTimeout(() => {
48 | this.setState({ visible: false });
49 | });
50 | }
51 |
52 | render() {
53 | return this.state.visible ? : null;
54 | }
55 | }
56 |
57 | const { getByTestId } = render( );
58 |
59 | await waitForElementToBeRemoved(() => getByTestId('view'), { timeout: 250 });
60 | });
61 |
--------------------------------------------------------------------------------
/src/lib/__tests__/wait-for-element.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { cleanup, render, waitForElement } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('waits for element to appear', async () => {
9 | const { rerender, getByTestId } = render( );
10 | const promise = waitForElement(() => getByTestId('test'));
11 | setTimeout(() => rerender( ));
12 | const element = await promise;
13 | expect(element).toBeTruthy();
14 | });
15 |
16 | test('can time out', async () => {
17 | jest.useFakeTimers();
18 | const promise = waitForElement(() => {});
19 | jest.advanceTimersByTime(4600);
20 | await expect(promise).rejects.toThrow(/timed out/i);
21 | jest.useRealTimers();
22 | });
23 |
24 | test('can specify our own timeout time', async () => {
25 | jest.useFakeTimers();
26 | const promise = waitForElement(() => {}, { timeout: 4700 });
27 | const handler = jest.fn();
28 | promise.then(handler, handler);
29 |
30 | jest.advanceTimersByTime(4600);
31 | expect(handler).toHaveBeenCalledTimes(0);
32 |
33 | jest.advanceTimersByTime(150);
34 |
35 | await expect(promise).rejects.toThrow(/timed out/i);
36 | jest.useRealTimers();
37 | });
38 |
39 | test('throws last thrown error', async () => {
40 | const { rerender } = render( );
41 | let error;
42 | setTimeout(() => {
43 | error = new Error('first error');
44 | rerender(first );
45 | }, 10);
46 | setTimeout(() => {
47 | error = new Error('second error');
48 | rerender(second );
49 | }, 20);
50 | const promise = waitForElement(
51 | () => {
52 | throw error;
53 | },
54 | { timeout: 50 },
55 | );
56 | await expect(promise).rejects.toThrow(error);
57 | });
58 |
59 | test('waits until callback does not return null', async () => {
60 | const { rerender, queryByTestId } = render( );
61 | const promise = waitForElement(() => queryByTestId('text'));
62 | rerender( );
63 | const element = await promise;
64 | expect(element).toBeTruthy();
65 | });
66 |
67 | test('throws error if no callback is provided', async () => {
68 | await expect(waitForElement()).rejects.toThrow(/callback/i);
69 | });
70 |
--------------------------------------------------------------------------------
/src/lib/__tests__/wait.js:
--------------------------------------------------------------------------------
1 | import { wait } from '../../';
2 |
3 | test('it waits for the data to be loaded', async () => {
4 | const spy = jest.fn();
5 | // we are using random timeout here to simulate a real-time example
6 | // of an async operation calling a callback at a non-deterministic time
7 | const randomTimeout = Math.floor(Math.random() * 60);
8 | setTimeout(spy, randomTimeout);
9 |
10 | await wait(() => expect(spy).toHaveBeenCalledTimes(1));
11 | expect(spy).toHaveBeenCalledWith();
12 | });
13 |
14 | test('wait defaults to a noop callback', async () => {
15 | const handler = jest.fn();
16 | Promise.resolve().then(handler);
17 | await wait();
18 | expect(handler).toHaveBeenCalledTimes(1);
19 | });
20 |
--------------------------------------------------------------------------------
/src/lib/__tests__/within.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, View } from 'react-native';
3 |
4 | import { cleanup, render, within } from '../../';
5 |
6 | afterEach(cleanup);
7 |
8 | test('it works when scoping to a smaller set of elements', () => {
9 | const { getByTestId } = render(
10 |
11 |
12 |
13 |
14 | ,
15 | );
16 |
17 | within(getByTestId('filter-box')).getByTestId('search-button');
18 | });
19 |
--------------------------------------------------------------------------------
/src/lib/config.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore next */
2 | let config = {
3 | asyncWrapper: cb => cb(),
4 | };
5 |
6 | function configure(newConfig) {
7 | if (typeof newConfig === 'function') {
8 | // Pass the existing config out to the provided function
9 | // and accept a delta in return
10 | newConfig = newConfig(config);
11 | }
12 |
13 | // Merge the incoming config delta
14 | config = {
15 | ...config,
16 | ...newConfig,
17 | };
18 | }
19 |
20 | function getConfig(key) {
21 | return key ? config[key] : config;
22 | }
23 |
24 | export { configure, getConfig };
25 |
--------------------------------------------------------------------------------
/src/lib/events.js:
--------------------------------------------------------------------------------
1 | const viewEvents = [
2 | 'accessibilityEscape',
3 | 'accessibilityTap',
4 | 'layout',
5 | 'magicTap',
6 | 'moveShouldSetResponder',
7 | 'moveShouldSetResponderCapture',
8 | 'responderGrant',
9 | 'responderMove',
10 | 'responderReject',
11 | 'responderRelease',
12 | 'responderTerminate',
13 | 'responderTerminationRequest',
14 | 'startShouldSetResponder',
15 | 'startShouldSetResponderCapture',
16 | ];
17 |
18 | const eventMap = {
19 | ActivityIndicator: [...viewEvents],
20 | Button: ['layout', 'press'],
21 | DrawerLayoutAndroid: [
22 | ...viewEvents,
23 | 'drawerClose',
24 | 'drawerOpen',
25 | 'drawerSlide',
26 | 'drawerStateChanged',
27 | ],
28 | Image: ['error', 'layout', 'load', 'loadEnd', 'loadStart', 'partialLoad', 'progress'],
29 | Modal: ['dismiss', 'orientationChange', 'requestClose', 'show'],
30 | Picker: [...viewEvents, 'valueChange'],
31 | Pressable: ['longPress', 'press', 'pressIn', 'pressOut'],
32 | RefreshControl: [...viewEvents, 'refresh'],
33 | SafeAreaView: [...viewEvents],
34 | ScrollView: [
35 | ...viewEvents,
36 | 'contentSizeChange',
37 | 'momentumScrollBegin',
38 | 'momentumScrollEnd',
39 | 'scroll',
40 | 'scrollBeginDrag',
41 | 'scrollEndDrag',
42 | ],
43 | Switch: [...viewEvents, 'valueChange'],
44 | Text: ['layout', 'longPress', 'press'],
45 | TextInput: [
46 | ...viewEvents,
47 | 'blur',
48 | 'change',
49 | 'changeText',
50 | 'contentSizeChange',
51 | 'endEditing',
52 | 'focus',
53 | 'keyPress',
54 | 'scroll',
55 | 'selectionChange',
56 | 'submitEditing',
57 | ],
58 | TouchableHighlight: [
59 | 'blur',
60 | 'focus',
61 | 'hideUnderlay',
62 | 'layout',
63 | 'longPress',
64 | 'press',
65 | 'pressIn',
66 | 'pressOut',
67 | 'showUnderlay',
68 | ],
69 | TouchableNativeFeedback: ['blur', 'focus', 'layout', 'longPress', 'press', 'pressIn', 'pressOut'],
70 | TouchableOpacity: ['blur', 'focus', 'layout', 'longPress', 'press', 'pressIn', 'pressOut'],
71 | TouchableWithoutFeedback: [
72 | 'blur',
73 | 'focus',
74 | 'layout',
75 | 'longPress',
76 | 'press',
77 | 'pressIn',
78 | 'pressOut',
79 | ],
80 | View: viewEvents,
81 | };
82 |
83 | class NativeTestEvent {
84 | constructor(typeArg, ...args) {
85 | this.args = args;
86 | this.typeArg = typeArg;
87 | this.validTargets = Object.keys(eventMap).filter(c => eventMap[c].includes(typeArg));
88 | }
89 |
90 | set target(target) {
91 | this._target = target;
92 | }
93 |
94 | get target() {
95 | return this._target;
96 | }
97 | }
98 |
99 | function getEventHandlerName(key) {
100 | return `on${key.charAt(0).toUpperCase()}${key.slice(1)}`;
101 | }
102 |
103 | function validateElementType(list, element) {
104 | return list.includes(element.type) || list.includes(element.type.displayName);
105 | }
106 |
107 | function isValidTarget(element, event) {
108 | return event.validTargets.length ? validateElementType(event.validTargets, element) : true;
109 | }
110 |
111 | function isDisabled(element) {
112 | const { accessibilityState = {}, disabled } = element.props;
113 | return disabled || accessibilityState.disabled;
114 | }
115 |
116 | function findEventTarget(element, event) {
117 | const { typeArg } = event;
118 | const handlerName = getEventHandlerName(typeArg);
119 | const eventHandler = element.props[handlerName];
120 |
121 | if (eventHandler && !isDisabled(element) && isValidTarget(element, event)) {
122 | return eventHandler;
123 | }
124 |
125 | return element.parent ? findEventTarget(element.parent, event) : null;
126 | }
127 |
128 | function fireEvent(element, event) {
129 | event.target = findEventTarget(element, event);
130 |
131 | if (event.target) event.target(...event.args);
132 |
133 | return event.target;
134 | }
135 |
136 | const eventList = Object.keys(eventMap).reduce((list, name) => {
137 | return [...list, ...eventMap[name].filter(event => !list.includes(event))];
138 | }, []);
139 |
140 | eventList.forEach(typeArg => {
141 | fireEvent[typeArg] = (node, ...args) => {
142 | return fireEvent(node, new NativeTestEvent(typeArg, ...args));
143 | };
144 | });
145 |
146 | export { eventMap, fireEvent, getEventHandlerName, NativeTestEvent };
147 |
--------------------------------------------------------------------------------
/src/lib/get-node-text.js:
--------------------------------------------------------------------------------
1 | function getNodeText(node) {
2 | if (node.type === 'Button') {
3 | return node.getProp('title');
4 | } else {
5 | return Array.from(node.children).join('');
6 | }
7 | }
8 |
9 | export { getNodeText };
10 |
--------------------------------------------------------------------------------
/src/lib/get-queries-for-element.js:
--------------------------------------------------------------------------------
1 | import * as defaultQueries from './queries';
2 |
3 | function getQueriesForElement(element, queries = defaultQueries) {
4 | return Object.keys(queries).reduce((helpers, key) => {
5 | const fn = queries[key];
6 | helpers[key] = fn.bind(null, element);
7 | return helpers;
8 | }, {});
9 | }
10 |
11 | export { getQueriesForElement };
12 |
--------------------------------------------------------------------------------
/src/lib/helpers.js:
--------------------------------------------------------------------------------
1 | function getSetImmediate() {
2 | if (typeof setImmediate === 'function') {
3 | return setImmediate;
4 | } else {
5 | return function setImmediate(fn) {
6 | return setTimeout(fn, 0);
7 | };
8 | }
9 | }
10 |
11 | export { getSetImmediate };
12 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import { getQueriesForElement } from './get-queries-for-element';
2 | import * as queries from './queries';
3 | import * as queryHelpers from './query-helpers';
4 |
5 | export * from './config';
6 | export * from './to-json';
7 | export * from './queries';
8 | export * from './wait';
9 | export * from './wait-for-element';
10 | export * from './wait-for-element-to-be-removed';
11 | export { getDefaultNormalizer } from './matches';
12 | export * from './get-node-text';
13 | export * from './events';
14 | export * from './get-queries-for-element';
15 | export * from './query-helpers';
16 | export * from './pretty-print';
17 |
18 | export {
19 | getQueriesForElement as within,
20 | // export query utils under a namespace for convenience:
21 | queries,
22 | queryHelpers,
23 | };
24 |
--------------------------------------------------------------------------------
/src/lib/matches.js:
--------------------------------------------------------------------------------
1 | function fuzzyMatches(textToMatch, node, matcher, normalizer) {
2 | if (typeof matcher === 'boolean' || typeof textToMatch === 'boolean') {
3 | return textToMatch == matcher;
4 | }
5 |
6 | if (!textToMatch && textToMatch !== '') {
7 | return false;
8 | }
9 |
10 | const normalizedText = normalizer(textToMatch);
11 | if (typeof matcher === 'string') {
12 | return normalizedText.toLowerCase().includes(matcher.toLowerCase());
13 | } else if (typeof matcher === 'function') {
14 | return matcher(normalizedText, node);
15 | } else {
16 | return matcher.test(normalizedText);
17 | }
18 | }
19 |
20 | function matches(textToMatch, node, matcher, normalizer) {
21 | if (typeof matcher === 'boolean' || typeof textToMatch === 'boolean') {
22 | return textToMatch === matcher;
23 | }
24 |
25 | if (!textToMatch && textToMatch !== '') {
26 | return false;
27 | }
28 |
29 | const normalizedText = normalizer(textToMatch);
30 | if (typeof matcher === 'string') {
31 | return normalizedText === matcher;
32 | } else if (typeof matcher === 'function') {
33 | return matcher(normalizedText, node);
34 | } else {
35 | return matcher.test(normalizedText);
36 | }
37 | }
38 |
39 | function getDefaultNormalizer({ trim = true, collapseWhitespace = true } = {}) {
40 | return text => {
41 | let normalizedText = text;
42 | normalizedText = trim ? normalizedText.trim() : normalizedText;
43 | normalizedText = collapseWhitespace ? normalizedText.replace(/\s+/g, ' ') : normalizedText;
44 | return normalizedText;
45 | };
46 | }
47 |
48 | function makeNormalizer({ trim, collapseWhitespace, normalizer }) {
49 | if (normalizer) {
50 | if (typeof trim !== 'undefined' || typeof collapseWhitespace !== 'undefined') {
51 | throw new Error(
52 | 'trim and collapseWhitespace are not supported with a normalizer. ' +
53 | 'If you want to use the default trim and collapseWhitespace logic in your normalizer, ' +
54 | 'use "getDefaultNormalizer({trim, collapseWhitespace})" and compose that into your normalizer',
55 | );
56 | }
57 |
58 | return normalizer;
59 | } else {
60 | return getDefaultNormalizer({ trim, collapseWhitespace });
61 | }
62 | }
63 |
64 | export { fuzzyMatches, matches, getDefaultNormalizer, makeNormalizer };
65 |
--------------------------------------------------------------------------------
/src/lib/pretty-print.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import prettyFormat from 'pretty-format';
3 |
4 | import { toJSON } from './to-json';
5 |
6 | const { ReactTestComponent, ReactElement } = prettyFormat.plugins;
7 |
8 | function prettyPrint(element, maxLength, options = {}) {
9 | let plugins = [ReactTestComponent, ReactElement];
10 | const { debug, ...rest } = options;
11 |
12 | const debugContent = prettyFormat(toJSON(element, debug), {
13 | plugins: plugins,
14 | printFunctionName: false,
15 | highlight: true,
16 | ...rest,
17 | });
18 |
19 | return maxLength !== undefined && debugContent && debugContent.toString().length > maxLength
20 | ? `${debugContent.slice(0, maxLength)}...`
21 | : debugContent;
22 | }
23 |
24 | export { prettyPrint };
25 |
--------------------------------------------------------------------------------
/src/lib/queries/all-utils.js:
--------------------------------------------------------------------------------
1 | export * from '../matches';
2 | export * from '../get-node-text';
3 | export * from '../query-helpers';
4 | export * from '../config';
5 |
--------------------------------------------------------------------------------
/src/lib/queries/display-value.js:
--------------------------------------------------------------------------------
1 | import {
2 | matches,
3 | fuzzyMatches,
4 | makeNormalizer,
5 | buildQueries,
6 | validComponentFilter,
7 | } from './all-utils';
8 |
9 | function queryAllByDisplayValue(
10 | container,
11 | value,
12 | { selector = n => n, exact = true, collapseWhitespace, trim, normalizer } = {},
13 | ) {
14 | const matcher = exact ? matches : fuzzyMatches;
15 | const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer });
16 |
17 | return Array.from(container.findAll(node => validComponentFilter(node, 'displayValueComponents')))
18 | .filter(selector)
19 | .filter(node => {
20 | if (node.type === 'Picker') {
21 | return matcher(node.getProp('selectedValue'), node, value, matchNormalizer);
22 | }
23 |
24 | return matcher(node.getProp('value'), node, value, matchNormalizer);
25 | });
26 | }
27 |
28 | const getMultipleError = (c, value) => `Found multiple elements with the value: ${value}.`;
29 | const getMissingError = (c, value) => `Unable to find an element with the value: ${value}.`;
30 | const [
31 | queryByDisplayValue,
32 | getAllByDisplayValue,
33 | getByDisplayValue,
34 | findAllByDisplayValue,
35 | findByDisplayValue,
36 | ] = buildQueries(queryAllByDisplayValue, getMultipleError, getMissingError);
37 |
38 | export {
39 | queryByDisplayValue,
40 | queryAllByDisplayValue,
41 | getByDisplayValue,
42 | getAllByDisplayValue,
43 | findAllByDisplayValue,
44 | findByDisplayValue,
45 | };
46 |
--------------------------------------------------------------------------------
/src/lib/queries/hint-text.js:
--------------------------------------------------------------------------------
1 | import { queryAllByProp, buildQueries } from './all-utils';
2 |
3 | const queryAllByHintText = queryAllByProp.bind(null, 'accessibilityHint');
4 |
5 | const getMultipleError = (c, hint) =>
6 | `Found multiple elements with the accessibilityHint of: ${hint}`;
7 | const getMissingError = (c, hint) =>
8 | `Unable to find an element with the accessibilityHint of: ${hint}`;
9 |
10 | const [
11 | queryByHintText,
12 | getAllByHintText,
13 | getByHintText,
14 | findAllByHintText,
15 | findByHintText,
16 | ] = buildQueries(queryAllByHintText, getMultipleError, getMissingError);
17 |
18 | export {
19 | queryByHintText,
20 | queryAllByHintText,
21 | getByHintText,
22 | getAllByHintText,
23 | findAllByHintText,
24 | findByHintText,
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/queries/index.js:
--------------------------------------------------------------------------------
1 | export * from './display-value';
2 | export * from './hint-text';
3 | export * from './label-text';
4 | export * from './placeholder-text';
5 | export * from './role';
6 | export * from './test-id';
7 | export * from './text';
8 | export * from './title';
9 |
--------------------------------------------------------------------------------
/src/lib/queries/label-text.js:
--------------------------------------------------------------------------------
1 | import { queryAllByProp, buildQueries } from './all-utils';
2 |
3 | const queryAllByLabelText = queryAllByProp.bind(null, 'accessibilityLabel');
4 |
5 | const getMultipleError = (c, label) =>
6 | `Found multiple elements with the accessibilityLabel of: ${label}`;
7 | const getMissingError = (c, label) =>
8 | `Unable to find an element with the accessibilityLabel of: ${label}`;
9 |
10 | const [
11 | queryByLabelText,
12 | getAllByLabelText,
13 | getByLabelText,
14 | findAllByLabelText,
15 | findByLabelText,
16 | ] = buildQueries(queryAllByLabelText, getMultipleError, getMissingError);
17 |
18 | export {
19 | queryByLabelText,
20 | queryAllByLabelText,
21 | getByLabelText,
22 | getAllByLabelText,
23 | findAllByLabelText,
24 | findByLabelText,
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/queries/placeholder-text.js:
--------------------------------------------------------------------------------
1 | import { queryAllByProp, buildQueries } from './all-utils';
2 |
3 | const queryAllByPlaceholderText = queryAllByProp.bind(null, 'placeholder');
4 |
5 | const getMultipleError = (c, text) =>
6 | `Found multiple elements with the placeholder text of: ${text}`;
7 | const getMissingError = (c, text) =>
8 | `Unable to find an element with the placeholder text of: ${text}`;
9 |
10 | const [
11 | queryByPlaceholderText,
12 | getAllByPlaceholderText,
13 | getByPlaceholderText,
14 | findAllByPlaceholderText,
15 | findByPlaceholderText,
16 | ] = buildQueries(queryAllByPlaceholderText, getMultipleError, getMissingError);
17 |
18 | export {
19 | queryByPlaceholderText,
20 | queryAllByPlaceholderText,
21 | getByPlaceholderText,
22 | getAllByPlaceholderText,
23 | findAllByPlaceholderText,
24 | findByPlaceholderText,
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/queries/role.js:
--------------------------------------------------------------------------------
1 | import { buildQueries } from './all-utils';
2 |
3 | const validRoles = [
4 | 'none',
5 | 'button',
6 | 'link',
7 | 'search',
8 | 'image',
9 | 'keyboardkey',
10 | 'text',
11 | 'adjustable',
12 | 'imagebutton',
13 | 'header',
14 | 'summary',
15 | 'alert',
16 | 'checkbox',
17 | 'combobox',
18 | 'menu',
19 | 'menubar',
20 | 'menuitem',
21 | 'progressbar',
22 | 'radio',
23 | 'radiogroup',
24 | 'scrollbar',
25 | 'spinbutton',
26 | 'switch',
27 | 'tab',
28 | 'tablist',
29 | 'timer',
30 | 'toolbar',
31 | ];
32 |
33 | const validTraits = [
34 | 'adjustable',
35 | 'allowsDirectInteraction',
36 | 'button',
37 | 'disabled',
38 | 'frequentUpdates',
39 | 'header',
40 | 'image',
41 | 'key',
42 | 'link',
43 | 'none',
44 | 'pageTurn',
45 | 'plays',
46 | 'search',
47 | 'selected',
48 | 'startsMedia',
49 | 'summary',
50 | 'text',
51 | ];
52 |
53 | function queryAllByRole(container, value, { selector = n => n } = {}) {
54 | const roleElements = container.findAll(c => c.getProp('accessibilityRole'));
55 | const traitElements = container.findAll(c => c.getProp('accessibilityTraits'));
56 |
57 | return [...roleElements, ...traitElements].filter(selector).filter(node => {
58 | const role = node.getProp('accessibilityRole');
59 | const traits = node.getProp('accessibilityTraits');
60 |
61 | if (role === value) {
62 | if (!validRoles.includes(value)) {
63 | throw new Error(
64 | `Found a match for accessibilityRole: "${value}", but "${value}" is not a valid accessibilityRole.`,
65 | );
66 | }
67 |
68 | return true;
69 | } else if (traits) {
70 | const arrayTraits = Array.isArray(traits) ? traits : [traits];
71 | const arrayValue = Array.isArray(value) ? value : [value];
72 | const traitMatch = arrayTraits.every(
73 | i => arrayValue.indexOf(i) > -1 && validTraits.includes(i),
74 | );
75 |
76 | if (traitMatch) {
77 | console.warn(
78 | `Found elements matching accessibilityTraits: \`${JSON.stringify(
79 | arrayValue,
80 | )}\`, which will soon be deprecated. Please transition to using accessibilityRoles.`,
81 | );
82 | }
83 |
84 | return traitMatch;
85 | }
86 |
87 | return false;
88 | });
89 | }
90 |
91 | const getMultipleError = (c, role) =>
92 | `Found multiple elements with the accessibilityRole of: ${role}`;
93 | const getMissingError = (c, role) =>
94 | `Unable to find an element with the accessibilityRole of: ${role}`;
95 |
96 | const [queryByRole, getAllByRole, getByRole, findAllByRole, findByRole] = buildQueries(
97 | queryAllByRole,
98 | getMultipleError,
99 | getMissingError,
100 | );
101 |
102 | export { queryByRole, queryAllByRole, getByRole, getAllByRole, findAllByRole, findByRole };
103 |
--------------------------------------------------------------------------------
/src/lib/queries/test-id.js:
--------------------------------------------------------------------------------
1 | import { queryAllByProp, buildQueries } from './all-utils';
2 |
3 | const queryAllByTestId = queryAllByProp.bind(null, 'testID');
4 |
5 | const getMultipleError = (c, id) => `Found multiple elements with the testID of: ${id}`;
6 | const getMissingError = (c, id) => `Unable to find an element with the testID of: ${id}`;
7 |
8 | const [queryByTestId, getAllByTestId, getByTestId, findAllByTestId, findByTestId] = buildQueries(
9 | queryAllByTestId,
10 | getMultipleError,
11 | getMissingError,
12 | );
13 |
14 | export {
15 | queryByTestId,
16 | queryAllByTestId,
17 | getByTestId,
18 | getAllByTestId,
19 | findAllByTestId,
20 | findByTestId,
21 | };
22 |
--------------------------------------------------------------------------------
/src/lib/queries/text.js:
--------------------------------------------------------------------------------
1 | import {
2 | fuzzyMatches,
3 | matches,
4 | makeNormalizer,
5 | getNodeText,
6 | buildQueries,
7 | validComponentFilter,
8 | } from './all-utils';
9 |
10 | function queryAllByText(
11 | container,
12 | text,
13 | { selector = n => n, exact = true, collapseWhitespace, trim, normalizer } = {},
14 | ) {
15 | const matcher = exact ? matches : fuzzyMatches;
16 | const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer });
17 |
18 | return Array.from(container.findAll(node => validComponentFilter(node, 'textComponents')))
19 | .filter(selector)
20 | .filter(node => matcher(getNodeText(node), node, text, matchNormalizer));
21 | }
22 |
23 | const getMultipleError = (c, text) => `Found multiple elements with the text: ${text}`;
24 | const getMissingError = (c, text) =>
25 | `Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`;
26 |
27 | const [queryByText, getAllByText, getByText, findAllByText, findByText] = buildQueries(
28 | queryAllByText,
29 | getMultipleError,
30 | getMissingError,
31 | );
32 |
33 | export { queryByText, queryAllByText, getByText, getAllByText, findAllByText, findByText };
34 |
--------------------------------------------------------------------------------
/src/lib/queries/title.js:
--------------------------------------------------------------------------------
1 | import {
2 | buildQueries,
3 | matches,
4 | fuzzyMatches,
5 | makeNormalizer,
6 | validComponentFilter,
7 | } from './all-utils';
8 |
9 | function queryAllByTitle(
10 | container,
11 | value,
12 | { selector = n => n, exact = true, collapseWhitespace, trim, normalizer } = {},
13 | ) {
14 | const matcher = exact ? matches : fuzzyMatches;
15 | const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer });
16 |
17 | return Array.from(container.findAll(node => validComponentFilter(node, 'titleComponents')))
18 | .filter(selector)
19 | .filter(node => matcher(node.getProp('title'), node, value, matchNormalizer));
20 | }
21 |
22 | const getMultipleError = (c, title) => `Found multiple elements with the title: ${title}`;
23 | const getMissingError = (c, title) => `Unable to find an element with the title: ${title}`;
24 |
25 | const [queryByTitle, getAllByTitle, getByTitle, findAllByTitle, findByTitle] = buildQueries(
26 | queryAllByTitle,
27 | getMultipleError,
28 | getMissingError,
29 | );
30 |
31 | export { queryByTitle, queryAllByTitle, getByTitle, getAllByTitle, findAllByTitle, findByTitle };
32 |
--------------------------------------------------------------------------------
/src/lib/query-helpers.js:
--------------------------------------------------------------------------------
1 | import { getConfig } from './config';
2 | import { prettyPrint } from './pretty-print';
3 | import { waitForElement } from './wait-for-element';
4 | import { fuzzyMatches, makeNormalizer, matches } from './matches';
5 |
6 | function debugTree(container) {
7 | const limit = process.env.DEBUG_PRINT_LIMIT || 7000;
8 |
9 | return prettyPrint(container, limit);
10 | }
11 |
12 | function getElementError(message, container) {
13 | return new Error([message, debugTree(container)].filter(Boolean).join('\n\n'));
14 | }
15 |
16 | function getMultipleElementsFoundError(message, container) {
17 | return getElementError(
18 | `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`,
19 | container,
20 | );
21 | }
22 |
23 | function validComponentFilter(node, key) {
24 | return key ? getConfig(key).includes(node.type) : typeof node.type === 'string';
25 | }
26 |
27 | function flatten(arr) {
28 | return arr.reduce(
29 | (flat, toFlatten) => flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten),
30 | [],
31 | );
32 | }
33 |
34 | function getChildren(node) {
35 | return node.children.map(child => {
36 | if (typeof child === 'string') {
37 | return child;
38 | } else if (validComponentFilter(child)) {
39 | return proxyElement(child);
40 | }
41 |
42 | return getChildren(child);
43 | });
44 | }
45 |
46 | function getParent(node) {
47 | if (node.parent) {
48 | return validComponentFilter(node.parent) ? proxyElement(node.parent) : getParent(node.parent);
49 | }
50 |
51 | return null;
52 | }
53 |
54 | function proxyElement(node) {
55 | // We take the guiding principles seriously around these parts. These methods just let
56 | // you do too much unfortunately, and they make it hard to follow the rules of the
57 | // testing-library. It's not that we don't trust you, in fact we do trust you! We've
58 | // left `findAll` on the instance for you as an emergency escape hatch for when
59 | // you're really in a bind. We do want you to test successfully, after all ☺️
60 | //
61 | // The main intent is to:
62 | // 1) Make it hard to assert on implementation details
63 | // 2) Force you to consider how to test from a user's perspective
64 | //
65 | // Of course if you can't figure that out, we still want you to be able to use this library,
66 | // so use `findAll`, just use it sparingly! We really believe you can do everything you
67 | // need using the query methods provided on the `render` API.
68 | return new Proxy(node, {
69 | get(target, key) {
70 | const ref = target[key];
71 |
72 | switch (key) {
73 | case 'findAll':
74 | return function(cb) {
75 | const overrideCb = n => cb(proxyElement(n));
76 | return ref.apply(this, [overrideCb]);
77 | };
78 | case 'getProp':
79 | return function(prop) {
80 | return target.props[prop];
81 | };
82 | case 'children':
83 | return flatten(getChildren(target));
84 | case 'parentNode':
85 | return getParent(target);
86 | case '$$typeof':
87 | return Symbol.for('ntl.element');
88 | case 'find':
89 | case 'findAllByProps':
90 | case 'findAllByType':
91 | case 'findByProps':
92 | case 'findByType':
93 | case 'instance':
94 | return undefined;
95 | default:
96 | // Let things behave normally if you're not running a query
97 | return ref;
98 | }
99 | },
100 | });
101 | }
102 |
103 | function queryAllByProp(
104 | prop,
105 | container,
106 | match,
107 | { selector = n => n, exact = true, collapseWhitespace, trim, normalizer } = {},
108 | ) {
109 | const matcher = exact ? matches : fuzzyMatches;
110 | const matchNormalizer = makeNormalizer({ collapseWhitespace, trim, normalizer });
111 |
112 | return Array.from(container.findAll(c => c.getProp(prop)))
113 | .filter(selector)
114 | .filter(node => {
115 | const value = node.getProp(prop);
116 |
117 | return matcher(value, container, match, matchNormalizer);
118 | });
119 | }
120 |
121 | function queryByProp(prop, container, match, ...args) {
122 | const els = queryAllByProp(prop, container, match, ...args);
123 | if (els.length > 1) {
124 | throw getMultipleElementsFoundError(`Found multiple elements by [${prop}=${match}]`, container);
125 | }
126 | return els[0] || null;
127 | }
128 |
129 | // accepts a query and returns a function that throws if more than one element is returned, otherwise
130 | // returns the result or null
131 | function makeSingleQuery(allQuery, getMultipleError) {
132 | return (container, ...args) => {
133 | const els = allQuery(container, ...args);
134 | if (els.length > 1) {
135 | throw getMultipleElementsFoundError(getMultipleError(container, ...args), container);
136 | }
137 | return els[0] || null;
138 | };
139 | }
140 |
141 | // accepts a query and returns a function that throws if an empty list is returned
142 | function makeGetAllQuery(allQuery, getMissingError) {
143 | return (container, ...args) => {
144 | const els = allQuery(container, ...args);
145 | if (!els.length) {
146 | throw getElementError(getMissingError(container, ...args), container);
147 | }
148 | return els;
149 | };
150 | }
151 |
152 | // accepts a getter and returns a function that calls waitForElement which invokes the getter.
153 | function makeFindQuery(getter) {
154 | return (container, text, options, waitForElementOptions) =>
155 | waitForElement(() => getter(container, text, options), waitForElementOptions);
156 | }
157 |
158 | function buildQueries(queryAllBy, getMultipleError, getMissingError) {
159 | const queryBy = makeSingleQuery(queryAllBy, getMultipleError);
160 | const getAllBy = makeGetAllQuery(queryAllBy, getMissingError);
161 | const getBy = makeSingleQuery(getAllBy, getMultipleError);
162 | const findAllBy = makeFindQuery(getAllBy);
163 | const findBy = makeFindQuery(getBy);
164 |
165 | return [queryBy, getAllBy, getBy, findAllBy, findBy];
166 | }
167 |
168 | export {
169 | buildQueries,
170 | getElementError,
171 | getMultipleElementsFoundError,
172 | makeFindQuery,
173 | makeGetAllQuery,
174 | makeSingleQuery,
175 | queryAllByProp,
176 | queryByProp,
177 | proxyElement,
178 | validComponentFilter,
179 | };
180 |
--------------------------------------------------------------------------------
/src/lib/to-json.js:
--------------------------------------------------------------------------------
1 | function toJSON({ _fiber: { stateNode = null } = {} } = {}, options = {}) {
2 | if (!stateNode) return null;
3 | if (stateNode.rootContainerInstance && stateNode.rootContainerInstance.children.length === 0)
4 | return null;
5 |
6 | return _toJSON(stateNode, options);
7 | }
8 |
9 | function _toJSON(inst, { omitProps = [] }) {
10 | if (inst.isHidden) {
11 | // Omit timed out children from output entirely. This seems like the least
12 | // surprising behavior. We could perhaps add a separate API that includes
13 | // them, if it turns out people need it.
14 | return null;
15 | }
16 | switch (inst.tag) {
17 | case 'TEXT':
18 | return inst.text;
19 | case 'INSTANCE': {
20 | /* eslint-disable no-unused-vars */
21 | // We don't include the `children` prop in JSON.
22 | // Instead, we will include the actual rendered children.
23 | const { children, ...props } = inst.props;
24 |
25 | // Convert all children to the JSON format
26 | const renderedChildren = inst.children.map(child => _toJSON(child, { omitProps }));
27 |
28 | // Function props get noisy in debug output, so we'll exclude them
29 | // Also exclude any props configured via options.omitProps
30 | let renderedProps = {};
31 | Object.keys(props).filter(name => {
32 | if (typeof props[name] !== 'function' && !omitProps.includes(name)) {
33 | renderedProps[name] = props[name];
34 | }
35 | });
36 |
37 | const json = {
38 | type: inst.type,
39 | props: renderedProps,
40 | children: renderedChildren,
41 | };
42 | Object.defineProperty(json, '$$typeof', {
43 | value: Symbol.for('react.test.json'),
44 | });
45 | return json;
46 | }
47 | default:
48 | throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
49 | }
50 | }
51 |
52 | export { toJSON };
53 |
--------------------------------------------------------------------------------
/src/lib/wait-for-element-to-be-removed.js:
--------------------------------------------------------------------------------
1 | import { getConfig } from './config';
2 | import { getSetImmediate } from './helpers';
3 |
4 | function waitForElementToBeRemoved(callback, { interval = 50, timeout = 4500 } = {}) {
5 | return new Promise((resolve, reject) => {
6 | if (typeof callback !== 'function') {
7 | reject(new Error('waitForElementToBeRemoved requires a callback as the first parameter'));
8 | return;
9 | }
10 | const timer = setTimeout(onTimeout, timeout);
11 | let observer;
12 |
13 | // Check if the element is not present
14 | /* istanbul ignore next */
15 | const result = callback();
16 | if (!result || (Array.isArray(result) && !result.length)) {
17 | onDone(
18 | new Error(
19 | 'The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal.',
20 | ),
21 | );
22 | }
23 |
24 | observer = setTimeout(onMutation);
25 |
26 | function onDone(error, result) {
27 | const setImmediate = getSetImmediate();
28 | clearTimeout(timer);
29 | /* istanbul ignore next */
30 | setImmediate(() => clearTimeout(observer));
31 | if (error) {
32 | reject(error);
33 | } else {
34 | resolve(result);
35 | }
36 | }
37 |
38 | function onMutation() {
39 | try {
40 | /* istanbul ignore next */
41 | const result = callback();
42 | if (!result || (Array.isArray(result) && !result.length)) {
43 | onDone(null, true);
44 | } else {
45 | observer = setTimeout(onMutation, interval);
46 | }
47 | } catch (error) {
48 | onDone(null, true);
49 | }
50 | }
51 |
52 | function onTimeout() {
53 | onDone(new Error('Timed out in waitForElementToBeRemoved.'), null);
54 | }
55 | });
56 | }
57 |
58 | function waitForElementToBeRemovedWrapper(...args) {
59 | return getConfig().asyncWrapper(() => waitForElementToBeRemoved(...args));
60 | }
61 |
62 | export { waitForElementToBeRemovedWrapper as waitForElementToBeRemoved };
63 |
--------------------------------------------------------------------------------
/src/lib/wait-for-element.js:
--------------------------------------------------------------------------------
1 | import { getConfig } from './config';
2 | import { getSetImmediate } from './helpers';
3 |
4 | function waitForElement(callback, { interval = 50, timeout = 4500 } = {}) {
5 | return new Promise((resolve, reject) => {
6 | if (typeof callback !== 'function') {
7 | reject(new Error('waitForElement requires a callback as the first parameter'));
8 | return;
9 | }
10 | const timer = setTimeout(onTimeout, timeout);
11 | let observer, lastError;
12 |
13 | function onDone(error, result) {
14 | const setImmediate = getSetImmediate();
15 | clearTimeout(timer);
16 | setImmediate(() => clearTimeout(observer));
17 | if (error) {
18 | reject(error);
19 | } else {
20 | resolve(result);
21 | }
22 | }
23 |
24 | function onMutation() {
25 | try {
26 | const result = callback();
27 | if (result) {
28 | onDone(null, result);
29 | }
30 | } catch (error) {
31 | lastError = error;
32 | observer = setTimeout(onMutation, interval);
33 | }
34 | }
35 |
36 | function onTimeout() {
37 | onDone(lastError || new Error('Timed out in waitForElement.'), null);
38 | }
39 |
40 | observer = setTimeout(onMutation);
41 | });
42 | }
43 |
44 | function waitForElementWrapper(...args) {
45 | return getConfig().asyncWrapper(() => waitForElement(...args));
46 | }
47 |
48 | export { waitForElementWrapper as waitForElement };
49 |
--------------------------------------------------------------------------------
/src/lib/wait.js:
--------------------------------------------------------------------------------
1 | import waitForExpect from 'wait-for-expect';
2 |
3 | import { getConfig } from './config';
4 |
5 | function wait(callback = () => {}, { timeout = 4500, interval = 50 } = {}) {
6 | return waitForExpect(callback, timeout, interval);
7 | }
8 |
9 | function waitWrapper(...args) {
10 | return getConfig().asyncWrapper(() => wait(...args));
11 | }
12 |
13 | export { waitWrapper as wait };
14 |
--------------------------------------------------------------------------------
/src/preset/configure.js:
--------------------------------------------------------------------------------
1 | import { asyncAct } from '../act-compat';
2 | import { NativeTestEvent } from '../lib/events';
3 | import { configure as configureNTL } from '../lib';
4 |
5 | // Make this global for convenience, just like browser events
6 | global.NativeTestEvent = NativeTestEvent;
7 |
8 | configureNTL({
9 | asyncWrapper: async cb => {
10 | let result;
11 | await asyncAct(async () => {
12 | result = await cb();
13 | });
14 | return result;
15 | },
16 |
17 | // Query lists
18 | coreComponents: [
19 | 'react-native/Libraries/Components/ActivityIndicator/ActivityIndicator',
20 | 'react-native/Libraries/Components/Button',
21 | 'react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid',
22 | 'react-native/Libraries/Image/Image',
23 | 'react-native/Libraries/Modal/Modal',
24 | 'react-native/Libraries/Components/Picker/Picker',
25 | 'react-native/Libraries/Components/Pressable/Pressable',
26 | 'react-native/Libraries/Components/RefreshControl/RefreshControl',
27 | 'react-native/Libraries/Components/SafeAreaView/SafeAreaView',
28 | 'react-native/Libraries/Components/ScrollView/ScrollView',
29 | 'react-native/Libraries/Components/Switch/Switch',
30 | 'react-native/Libraries/Text/Text',
31 | 'react-native/Libraries/Components/TextInput/TextInput',
32 | 'react-native/Libraries/Components/Touchable/TouchableHighlight',
33 | 'react-native/Libraries/Components/Touchable/TouchableNativeFeedback',
34 | 'react-native/Libraries/Components/Touchable/TouchableOpacity',
35 | 'react-native/Libraries/Components/Touchable/TouchableWithoutFeedback',
36 | 'react-native/Libraries/Components/View/View',
37 | ],
38 | displayValueComponents: ['TextInput', 'Picker', 'Switch'],
39 | textComponents: ['Button', 'Text', 'TextInput'],
40 | titleComponents: ['Button', 'RefreshControl'],
41 | });
42 |
--------------------------------------------------------------------------------
/src/preset/mock-component.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { mockNativeMethods } from './mock-native-methods';
4 |
5 | function mockComponent(path, { instanceMethods, displayName: customDisplayName } = {}) {
6 | const RealComponent = jest.requireActual(path);
7 |
8 | const displayName =
9 | customDisplayName ||
10 | RealComponent.displayName ||
11 | RealComponent.name ||
12 | (RealComponent.render // handle React.forwardRef
13 | ? RealComponent.render.displayName || RealComponent.render.name
14 | : 'Unknown');
15 |
16 | const SuperClass = typeof RealComponent === 'function' ? RealComponent : React.Component;
17 |
18 | class Component extends SuperClass {
19 | static displayName = displayName;
20 |
21 | render() {
22 | const props = Object.assign({}, RealComponent.defaultProps);
23 | if (this.props) {
24 | Object.keys(this.props).forEach(prop => {
25 | // We can't just assign props on top of defaultProps
26 | // because React treats undefined as special and different from null.
27 | // If a prop is specified but set to undefined it is ignored and the
28 | // default prop is used instead. If it is set to null, then the
29 | // null value overwrites the default value.
30 | if (this.props[prop] !== undefined) {
31 | props[prop] = this.props[prop];
32 | }
33 | });
34 | }
35 |
36 | return React.createElement(displayName, props, this.props.children);
37 | }
38 | }
39 |
40 | Object.keys(RealComponent).forEach(classStatic => {
41 | Component[classStatic] = RealComponent[classStatic];
42 | });
43 |
44 | Object.assign(Component.prototype, mockNativeMethods);
45 |
46 | if (instanceMethods != null) {
47 | Object.assign(Component.prototype, instanceMethods);
48 | }
49 |
50 | return Component;
51 | }
52 |
53 | export { mockComponent };
54 |
--------------------------------------------------------------------------------
/src/preset/mock-modules.js:
--------------------------------------------------------------------------------
1 | import { getConfig } from '../lib';
2 | import { mockComponent } from './mock-component';
3 |
4 | import { mockScrollView } from './mock-scroll-view';
5 | import RefreshControlMock from './mock-refresh-control';
6 |
7 | // Un-mock the react-native components so we can do it ourselves
8 | getConfig('coreComponents').forEach(component => {
9 | try {
10 | // try to un-mock the component
11 | jest.unmock(component);
12 | } catch (e) {
13 | // do nothing if we can't
14 | }
15 | });
16 |
17 | // Un-mock ReactNative so we can hide annoying `console.warn`s
18 | jest.unmock('react-native/Libraries/Renderer/shims/ReactNative');
19 |
20 | // Mock the components we want mocked
21 | getConfig('coreComponents').forEach(component => {
22 | try {
23 | jest.doMock(component, () => mockComponent(component));
24 | } catch (e) {}
25 | });
26 |
27 | // Mock the Picker one-off because it's kinda weird
28 | jest.doMock('react-native/Libraries/Components/Picker/Picker', () => {
29 | const React = jest.requireActual('react');
30 | const Picker = mockComponent('react-native/Libraries/Components/Picker/Picker');
31 | Picker.Item = ({ children, ...props }) => React.createElement('Picker.Item', props, children);
32 | return Picker;
33 | });
34 |
35 | // Mock some other tricky ones
36 | jest.doMock('react-native/Libraries/Components/ScrollView/ScrollView', () => {
37 | const baseComponent = mockComponent('react-native/Libraries/Components/ScrollView/ScrollView', {
38 | instanceMethods: {
39 | getScrollResponder: jest.fn(),
40 | getScrollableNode: jest.fn(),
41 | getInnerViewNode: jest.fn(),
42 | getInnerViewRef: jest.fn(),
43 | getNativeScrollRef: jest.fn(),
44 | scrollTo: jest.fn(),
45 | scrollToEnd: jest.fn(),
46 | flashScrollIndicators: jest.fn(),
47 | scrollResponderZoomTo: jest.fn(),
48 | scrollResponderScrollNativeHandleToKeyboard: jest.fn(),
49 | },
50 | });
51 | return mockScrollView(baseComponent);
52 | });
53 |
54 | jest.doMock(
55 | 'react-native/Libraries/Components/RefreshControl/RefreshControl',
56 | () => RefreshControlMock,
57 | );
58 |
59 | jest.doMock('react-native/Libraries/Components/TextInput/TextInput', () =>
60 | mockComponent('react-native/Libraries/Components/TextInput/TextInput', {
61 | instanceMethods: {
62 | isFocused: jest.fn(),
63 | clear: jest.fn(),
64 | getNativeRef: jest.fn(),
65 | },
66 | }),
67 | );
68 |
69 | jest.doMock('react-native/Libraries/Components/Touchable/TouchableHighlight', () =>
70 | mockComponent('react-native/Libraries/Components/Touchable/TouchableHighlight', {
71 | displayName: 'TouchableHighlight',
72 | }),
73 | );
74 |
75 | jest.doMock('react-native/Libraries/Components/Touchable/TouchableOpacity', () =>
76 | mockComponent('react-native/Libraries/Components/Touchable/TouchableOpacity', {
77 | displayName: 'TouchableOpacity',
78 | }),
79 | );
80 |
81 | // Re-mock ReactNative
82 | jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
83 | jest.mock('react-native/Libraries/Renderer/shims/ReactNative');
84 |
85 | // Mock LogBox
86 | jest.mock('react-native/Libraries/LogBox/LogBox');
87 |
--------------------------------------------------------------------------------
/src/preset/mock-native-methods.js:
--------------------------------------------------------------------------------
1 | const mockNativeMethods = {
2 | measure: jest.fn(),
3 | measureInWindow: jest.fn(),
4 | measureLayout: jest.fn(),
5 | setNativeProps: jest.fn(),
6 | focus: jest.fn(),
7 | blur: jest.fn(),
8 | };
9 |
10 | export { mockNativeMethods };
11 |
--------------------------------------------------------------------------------
/src/preset/mock-refresh-control.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | const requireNativeComponent = jest.requireActual(
4 | 'react-native/Libraries/ReactNative/requireNativeComponent',
5 | );
6 | const RCTRefreshControl = requireNativeComponent('RCTRefreshControl');
7 |
8 | class RefreshControlMock extends React.Component {
9 | static latestRef;
10 |
11 | componentDidMount() {
12 | RefreshControlMock.latestRef = this;
13 | }
14 | render() {
15 | return ;
16 | }
17 | }
18 |
19 | module.exports = RefreshControlMock;
20 |
--------------------------------------------------------------------------------
/src/preset/mock-scroll-view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const View = require('react-native/Libraries/Components/View/View');
3 |
4 | const requireNativeComponent = jest.requireActual(
5 | 'react-native/Libraries/ReactNative/requireNativeComponent',
6 | );
7 | const RCTScrollView = requireNativeComponent('RCTScrollView');
8 |
9 | function mockScrollView(BaseComponent) {
10 | class ScrollViewMock extends BaseComponent {
11 | render() {
12 | return (
13 |
14 | {this.props.refreshControl}
15 | {this.props.children}
16 |
17 | );
18 | }
19 | }
20 | return ScrollViewMock;
21 | }
22 |
23 | export { mockScrollView };
24 |
--------------------------------------------------------------------------------
/src/preset/serializer.js:
--------------------------------------------------------------------------------
1 | import { toJSON } from '../lib';
2 |
3 | module.exports = {
4 | test(value) {
5 | return value && value.$$typeof && Symbol.keyFor(value.$$typeof) === 'ntl.element';
6 | },
7 | print(value, serialize) {
8 | return serialize(toJSON(value));
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/preset/setup.js:
--------------------------------------------------------------------------------
1 | // Configure NTL
2 | import './configure';
3 |
4 | // Un-mock the things that we'll be mocking
5 | import './mock-modules';
6 |
--------------------------------------------------------------------------------
/typings/config.d.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | asyncWrapper(cb: Function): Promise;
3 | }
4 |
5 | export const getConfig: () => Config;
6 |
--------------------------------------------------------------------------------
/typings/events.d.ts:
--------------------------------------------------------------------------------
1 | import { NativeTestInstance } from './query-helpers';
2 |
3 | export type EventType =
4 | | 'focus'
5 | | 'blur'
6 | | 'change'
7 | | 'changeText'
8 | | 'valueChange'
9 | | 'contentSizeChange'
10 | | 'endEditing'
11 | | 'keyPress'
12 | | 'submitEditing'
13 | | 'layout'
14 | | 'selectionChange'
15 | | 'longPress'
16 | | 'press'
17 | | 'pressIn'
18 | | 'pressOut'
19 | | 'momentumScrollBegin'
20 | | 'momentumScrollEnd'
21 | | 'scroll'
22 | | 'scrollBeginDrag'
23 | | 'scrollEndDrag'
24 | | 'load'
25 | | 'error'
26 | | 'progress'
27 | | 'custom';
28 |
29 | export declare class NativeTestEvent {
30 | constructor(typeArg: string, ...params: any[]);
31 | }
32 |
33 | export type FireFunction = (element: NativeTestInstance, event: NativeTestEvent) => boolean;
34 | export type FireObject = {
35 | [K in EventType]: (element: NativeTestInstance, options?: {}) => boolean
36 | };
37 |
38 | export const getEventHandlerName: (key: string) => string;
39 | export const fireEvent: FireFunction & FireObject;
40 |
--------------------------------------------------------------------------------
/typings/get-node-text.d.ts:
--------------------------------------------------------------------------------
1 | import { NativeTestInstance } from './query-helpers';
2 |
3 | export declare function getNodeText(node: NativeTestInstance): string;
4 |
--------------------------------------------------------------------------------
/typings/get-queries-for-element.d.ts:
--------------------------------------------------------------------------------
1 | import * as queries from './queries';
2 | import { NativeTestInstance } from './query-helpers';
3 |
4 | export type BoundFunction = T extends (
5 | prop: string,
6 | element: NativeTestInstance,
7 | text: infer P,
8 | options: infer Q,
9 | ) => infer R
10 | ? (text: P, options?: Q) => R
11 | : T extends (a1: any, text: infer P, options: infer Q) => infer R
12 | ? (text: P, options?: Q) => R
13 | : never;
14 | export type BoundFunctions = { [P in keyof T]: BoundFunction };
15 |
16 | interface Query extends Function {
17 | (container: NativeTestInstance, ...args: any[]):
18 | | Error
19 | | Promise
20 | | Promise
21 | | NativeTestInstance[]
22 | | NativeTestInstance
23 | | null;
24 | }
25 |
26 | interface Queries {
27 | [T: string]: Query;
28 | }
29 |
30 | export function getQueriesForElement(
31 | element: NativeTestInstance,
32 | queriesToBind?: T,
33 | ): BoundFunctions;
34 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement, ComponentType } from 'react';
2 | import { act as reactAct } from 'react-test-renderer';
3 |
4 | import * as queries from './queries';
5 | import * as queryHelpers from './query-helpers';
6 | import { NativeTestInstance } from './query-helpers';
7 | import { NativeTestInstanceJSON } from './to-json';
8 | import { getQueriesForElement, BoundFunction } from './get-queries-for-element';
9 |
10 | declare const within: typeof getQueriesForElement;
11 |
12 | interface Query extends Function {
13 | (container: NativeTestInstance, ...args: any[]):
14 | | Error
15 | | Promise
16 | | Promise
17 | | NativeTestInstance[]
18 | | NativeTestInstance
19 | | null;
20 | }
21 |
22 | interface Queries {
23 | [T: string]: Query;
24 | }
25 |
26 | export type RenderResult = {
27 | baseElement: NativeTestInstance;
28 | container: NativeTestInstance;
29 | debug: (element?: NativeTestInstance) => void;
30 | rerender: (ui: ReactElement) => void;
31 | unmount: () => void;
32 | asJSON: () => NativeTestInstanceJSON;
33 | } & { [P in keyof Q]: BoundFunction };
34 |
35 | export interface RenderOptions {
36 | queries?: Q;
37 | wrapper?: ComponentType;
38 | options?: {
39 | debug?: DebugOptions;
40 | };
41 | }
42 |
43 | export interface DebugOptions {
44 | omitProps: string[];
45 | }
46 |
47 | type Omit = Pick>;
48 |
49 | export function render(
50 | ui: ReactElement,
51 | options?: Omit,
52 | ): RenderResult;
53 | export function render(
54 | ui: ReactElement,
55 | options: RenderOptions,
56 | ): RenderResult;
57 |
58 | export const cleanup: () => void;
59 |
60 | export const act: typeof reactAct extends undefined
61 | ? (callback: () => void) => void
62 | : typeof reactAct;
63 |
64 | export { queries, queryHelpers, within };
65 | export * from './to-json';
66 | export * from './config';
67 | export * from './events';
68 | export * from './get-node-text';
69 | export * from './get-queries-for-element';
70 | export * from './matches';
71 | export * from './pretty-print';
72 | export * from './queries';
73 | export * from './query-helpers';
74 | export * from './wait';
75 | export * from './wait-for-element';
76 | export * from './wait-for-element-to-be-removed';
77 |
--------------------------------------------------------------------------------
/typings/matches.d.ts:
--------------------------------------------------------------------------------
1 | import { NativeTestInstance } from './query-helpers';
2 |
3 | export type MatcherFunction = (content: string, element: HTMLElement) => boolean;
4 | export type Matcher = boolean | string | RegExp | MatcherFunction;
5 |
6 | export type NormalizerFn = (text: string) => string;
7 | export type SelectorFn = (element: NativeTestInstance) => boolean;
8 |
9 | export interface MatcherOptions {
10 | exact?: boolean;
11 | /** Use normalizer with getDefaultNormalizer instead */
12 | trim?: boolean;
13 | /** Use normalizer with getDefaultNormalizer instead */
14 | collapseWhitespace?: boolean;
15 | selector?: SelectorFn;
16 | normalizer?: NormalizerFn;
17 | }
18 |
19 | export type Match = (
20 | textToMatch: string,
21 | node: HTMLElement | null,
22 | matcher: Matcher,
23 | options?: MatcherOptions,
24 | ) => boolean;
25 |
26 | export interface DefaultNormalizerOptions {
27 | trim?: boolean;
28 | collapseWhitespace?: boolean;
29 | }
30 |
31 | export declare function getDefaultNormalizer(options?: DefaultNormalizerOptions): NormalizerFn;
32 |
33 | // N.B. Don't expose fuzzyMatches + matches here: they're not public API
34 |
--------------------------------------------------------------------------------
/typings/pretty-print.d.ts:
--------------------------------------------------------------------------------
1 | import { NativeTestInstance } from './query-helpers';
2 | import { DebugOptions } from '.';
3 |
4 | export function prettyPrint(
5 | element: NativeTestInstance | string,
6 | maxLength?: number,
7 | options?: {
8 | debug: DebugOptions;
9 | },
10 | ): string | false;
11 |
--------------------------------------------------------------------------------
/typings/queries.d.ts:
--------------------------------------------------------------------------------
1 | import { Matcher, MatcherOptions } from './matches';
2 | import { WaitForElementOptions } from './wait-for-element';
3 | import { NativeTestInstance, SelectorMatcherOptions } from './query-helpers';
4 |
5 | export type QueryByBoundProp = (
6 | container: NativeTestInstance,
7 | id: Matcher,
8 | options?: MatcherOptions,
9 | ) => NativeTestInstance | null;
10 |
11 | export type AllByBoundProp = (
12 | container: NativeTestInstance,
13 | id: Matcher,
14 | options?: MatcherOptions,
15 | ) => NativeTestInstance[];
16 |
17 | export type FindAllByBoundProp = (
18 | container: NativeTestInstance,
19 | id: Matcher,
20 | options?: MatcherOptions,
21 | ) => Promise;
22 |
23 | export type GetByBoundProp = (
24 | container: NativeTestInstance,
25 | id: Matcher,
26 | options?: MatcherOptions,
27 | ) => NativeTestInstance;
28 |
29 | export type FindByBoundProp = (
30 | container: NativeTestInstance,
31 | id: Matcher,
32 | options?: MatcherOptions,
33 | waitForElementOptions?: WaitForElementOptions,
34 | ) => Promise;
35 |
36 | export type QueryByText = (
37 | container: NativeTestInstance,
38 | id: Matcher,
39 | options?: SelectorMatcherOptions,
40 | ) => NativeTestInstance | null;
41 |
42 | export type AllByText = (
43 | container: NativeTestInstance,
44 | id: Matcher,
45 | options?: SelectorMatcherOptions,
46 | ) => NativeTestInstance[];
47 |
48 | export type FindAllByText = (
49 | container: NativeTestInstance,
50 | id: Matcher,
51 | options?: MatcherOptions,
52 | waitForElementOptions?: WaitForElementOptions,
53 | ) => Promise;
54 |
55 | export type GetByText = (
56 | container: NativeTestInstance,
57 | id: Matcher,
58 | options?: MatcherOptions,
59 | ) => NativeTestInstance;
60 |
61 | export type FindByText = (
62 | container: NativeTestInstance,
63 | id: Matcher,
64 | options?: MatcherOptions,
65 | waitForElementOptions?: WaitForElementOptions,
66 | ) => Promise;
67 |
68 | export const getByDisplayValue: GetByBoundProp;
69 | export const getByHintText: GetByBoundProp;
70 | export const getByLabelText: GetByBoundProp;
71 | export const getByRole: GetByBoundProp;
72 | export const getByPlaceholderText: GetByBoundProp;
73 | export const getByTestId: GetByBoundProp;
74 | export const getByText: GetByText;
75 | export const getByTitle: GetByBoundProp;
76 |
77 | export const getAllByDisplayValue: AllByBoundProp;
78 | export const getAllByHintText: AllByBoundProp;
79 | export const getAllByLabelText: AllByBoundProp;
80 | export const getAllByRole: AllByBoundProp;
81 | export const getAllByPlaceholderText: AllByBoundProp;
82 | export const getAllByTestId: AllByBoundProp;
83 | export const getAllByText: AllByText;
84 | export const getAllByTitle: AllByBoundProp;
85 |
86 | export const queryByDisplayValue: QueryByBoundProp;
87 | export const queryByHintText: QueryByBoundProp;
88 | export const queryByLabelText: QueryByBoundProp;
89 | export const queryByRole: QueryByBoundProp;
90 | export const queryByPlaceholderText: QueryByBoundProp;
91 | export const queryByTestId: QueryByBoundProp;
92 | export const queryByText: QueryByText;
93 | export const queryByTitle: QueryByBoundProp;
94 |
95 | export const queryAllByDisplayValue: AllByBoundProp;
96 | export const queryAllByHintText: AllByBoundProp;
97 | export const queryAllByLabelText: AllByBoundProp;
98 | export const queryAllByRole: AllByBoundProp;
99 | export const queryAllByPlaceholderText: AllByBoundProp;
100 | export const queryAllByTestId: AllByBoundProp;
101 | export const queryAllByText: AllByText;
102 | export const queryAllByTitle: AllByBoundProp;
103 |
104 | export const findByDisplayValue: FindByBoundProp;
105 | export const findByHintText: FindByBoundProp;
106 | export const findByLabelText: FindByBoundProp;
107 | export const findByRole: FindByBoundProp;
108 | export const findByPlaceholderText: FindByBoundProp;
109 | export const findByTestId: FindByBoundProp;
110 | export const findByText: FindByText;
111 | export const findByTitle: FindByBoundProp;
112 |
113 | export const findAllByDisplayValue: FindAllByBoundProp;
114 | export const findAllByHintText: FindAllByBoundProp;
115 | export const findAllByLabelText: FindAllByBoundProp;
116 | export const findAllByRole: FindAllByBoundProp;
117 | export const findAllByPlaceholderText: FindAllByBoundProp;
118 | export const findAllByTestId: FindAllByBoundProp;
119 | export const findAllByText: FindAllByText;
120 | export const findAllByTitle: FindAllByBoundProp;
121 |
--------------------------------------------------------------------------------
/typings/query-helpers.d.ts:
--------------------------------------------------------------------------------
1 | import { ReactTestInstance } from 'react-test-renderer';
2 |
3 | import { Matcher, MatcherOptions } from './matches';
4 |
5 | type Omit = Pick>;
6 |
7 | export type SelectorMatcherOptions = Omit & {
8 | selector?: string;
9 | };
10 |
11 | type ReactTestInstanceExtended = ReactTestInstance & {
12 | getProp: (name: string) => NativeTestInstance;
13 | parentNode: NativeTestInstance;
14 | };
15 |
16 | export type NativeTestInstance = Omit<
17 | ReactTestInstanceExtended,
18 | 'findAllByProps' | 'findAllByType' | 'findByProps' | 'findByType' | 'instance'
19 | >;
20 |
21 | export type QueryByProp = (
22 | attribute: string,
23 | container: NativeTestInstance,
24 | id: Matcher,
25 | options?: MatcherOptions,
26 | ) => NativeTestInstance | null;
27 |
28 | export type AllByProp = (
29 | attribute: string,
30 | container: HTMLElement,
31 | id: Matcher,
32 | options?: MatcherOptions,
33 | ) => NativeTestInstance[];
34 |
35 | // TODO: finish types of the rest of the helpers
36 | export const getElementError: (message: string, container: NativeTestInstance) => Error;
37 | export const queryAllByProp: AllByProp;
38 | export const queryByProp: QueryByProp;
39 | export const proxyElement: (node: ReactTestInstance) => NativeTestInstance;
40 |
--------------------------------------------------------------------------------
/typings/to-json.d.ts:
--------------------------------------------------------------------------------
1 | import { NativeTestInstance } from './query-helpers';
2 |
3 | export interface NativeTestInstanceJSON {
4 | type: string;
5 | props: { [propName: string]: any };
6 | children: null | Array;
7 | $$typeof?: Symbol;
8 | }
9 |
10 | export const toJSON: (node: NativeTestInstance) => NativeTestInstanceJSON;
11 |
--------------------------------------------------------------------------------
/typings/wait-for-element-to-be-removed.d.ts:
--------------------------------------------------------------------------------
1 | export function waitForElementToBeRemoved(
2 | callback: () => T,
3 | options?: {
4 | timeout?: number;
5 | interval?: number;
6 | },
7 | ): Promise;
8 |
--------------------------------------------------------------------------------
/typings/wait-for-element.d.ts:
--------------------------------------------------------------------------------
1 | export interface WaitForElementOptions {
2 | timeout?: number;
3 | interval?: number;
4 | }
5 |
6 | export function waitForElement(callback: () => T, options?: WaitForElementOptions): Promise;
7 |
--------------------------------------------------------------------------------
/typings/wait.d.ts:
--------------------------------------------------------------------------------
1 | export function wait(
2 | callback?: () => void,
3 | options?: {
4 | timeout?: number;
5 | interval?: number;
6 | },
7 | ): Promise;
8 |
--------------------------------------------------------------------------------