├── .all-contributorsrc
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── Bug_Report.md
│ ├── Feature_Request.md
│ └── Question.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ └── validate.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── babel.config.js
├── extend-expect.d.ts
├── extend-expect.js
├── jest.config.js
├── legacy-extend-expect.d.ts
├── legacy-extend-expect.js
├── other
└── eagle.png
├── package.json
├── setup-tests.ts
├── src
├── __tests__
│ ├── __snapshots__
│ │ ├── to-be-visible.tsx.snap
│ │ └── to-have-style.tsx.snap
│ ├── component-tree.tsx
│ ├── legacy-extend-expect.tsx
│ ├── to-be-disabled.tsx
│ ├── to-be-empty-element.tsx
│ ├── to-be-empty.tsx
│ ├── to-be-on-the-screen-import.tsx
│ ├── to-be-on-the-screen.tsx
│ ├── to-be-visible.tsx
│ ├── to-contain-element.tsx
│ ├── to-have-accessibility-state.tsx
│ ├── to-have-accessibility-value.tsx
│ ├── to-have-prop.tsx
│ ├── to-have-style.tsx
│ ├── to-have-text-content.tsx
│ └── utils.ts
├── __types__
│ ├── jest-explicit-extend.test-d.ts
│ └── jest-implicit-extend.test-d.ts
├── component-tree.ts
├── extend-expect.ts
├── index.ts
├── legacy-extend-expect.ts
├── to-be-disabled.ts
├── to-be-empty-element.ts
├── to-be-on-the-screen.ts
├── to-be-visible.ts
├── to-contain-element.ts
├── to-have-accessibility-state.ts
├── to-have-accessibility-value.ts
├── to-have-prop.ts
├── to-have-style.ts
├── to-have-text-content.ts
└── utils.ts
├── tsconfig.json
└── tsconfig.prod.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "jest-native",
3 | "projectOwner": "testing-library",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": true,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "bcarroll22",
15 | "name": "Brandon Carroll",
16 | "avatar_url": "https://avatars2.githubusercontent.com/u/11020406?v=4",
17 | "profile": "https://github.com/bcarroll22",
18 | "contributions": [
19 | "code",
20 | "doc",
21 | "infra",
22 | "test"
23 | ]
24 | },
25 | {
26 | "login": "SantiMA10",
27 | "name": "Santi",
28 | "avatar_url": "https://avatars2.githubusercontent.com/u/7255298?v=4",
29 | "profile": "http://santiagomartin.dev",
30 | "contributions": [
31 | "code"
32 | ]
33 | },
34 | {
35 | "login": "marnusw",
36 | "name": "Marnus Weststrate",
37 | "avatar_url": "https://avatars0.githubusercontent.com/u/971499?v=4",
38 | "profile": "https://github.com/marnusw",
39 | "contributions": [
40 | "code"
41 | ]
42 | },
43 | {
44 | "login": "Shywim",
45 | "name": "Matthieu Harlé",
46 | "avatar_url": "https://avatars3.githubusercontent.com/u/1584563?v=4",
47 | "profile": "https://github.com/Shywim",
48 | "contributions": [
49 | "code"
50 | ]
51 | },
52 | {
53 | "login": "acatalina",
54 | "name": "Alvaro Catalina",
55 | "avatar_url": "https://avatars3.githubusercontent.com/u/23233812?v=4",
56 | "profile": "https://github.com/acatalina",
57 | "contributions": [
58 | "code"
59 | ]
60 | },
61 | {
62 | "login": "ilkeryilmaz",
63 | "name": "ilker Yılmaz",
64 | "avatar_url": "https://avatars1.githubusercontent.com/u/1588236?v=4",
65 | "profile": "http://www.ilkeryilmaz.com",
66 | "contributions": [
67 | "doc"
68 | ]
69 | },
70 | {
71 | "login": "donovanhiland",
72 | "name": "Donovan Hiland",
73 | "avatar_url": "https://avatars2.githubusercontent.com/u/17991396?v=4",
74 | "profile": "https://github.com/donovanhiland",
75 | "contributions": [
76 | "code",
77 | "test"
78 | ]
79 | }
80 | ],
81 | "contributorsPerLine": 7,
82 | "skipCi": true
83 | }
84 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.js text eol=lf
3 |
--------------------------------------------------------------------------------
/.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 | - `@testing-library/react-native` 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/testing-library
19 | - Stack Overflow https://stackoverflow.com/questions/tagged/react-testing-library
20 |
21 | ## ❓ 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 [docs](https://github.com/testing-library/jest-native/README.md)
30 | - [ ] Typescript definitions updated
31 | - [ ] Tests
32 | - [ ] Ready to be merged
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: daily
7 |
8 | - package-ecosystem: npm
9 | directory: /
10 | schedule:
11 | interval: daily
12 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: validate
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: ['**']
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | main:
15 | strategy:
16 | matrix:
17 | node: [14, 16, 18]
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: ⬇️ Checkout repo
21 | uses: actions/checkout@v3
22 | with:
23 | fetch-depth: 0
24 |
25 | - name: ⎔ Setup node
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node }}
29 |
30 | - name: 📥 Download deps
31 | run: yarn install
32 |
33 | - name: ▶️ Run linter
34 | run: yarn lint
35 |
36 | - name: ▶️ Run typecheck
37 | run: yarn typecheck
38 |
39 | - name: ▶️ Run tests with coverage
40 | run: yarn test:coverage
41 |
42 | - name: ⬆️ Upload coverage report
43 | uses: codecov/codecov-action@v3
44 | with:
45 | fail_ci_if_error: true
46 | flags: node-${{ matrix.node }}
47 |
48 | release:
49 | needs: main
50 | runs-on: ubuntu-latest
51 | if:
52 | ${{ github.repository == 'testing-library/jest-native' && github.ref == 'refs/heads/main' &&
53 | github.event_name == 'push' }}
54 | steps:
55 | - name: ⬇️ Checkout repo
56 | uses: actions/checkout@v3
57 |
58 | - name: ⎔ Setup node
59 | uses: actions/setup-node@v3
60 | with:
61 | node-version: 18
62 |
63 | - name: 📥 Download deps
64 | uses: bahmutov/npm-install@v1
65 | with:
66 | useLockFile: false
67 |
68 | - name: 🚀 Release
69 | run: yarn semantic-release
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
73 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | yarn pretty-quick --staged
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The changelog is automatically updated using
4 | [semantic-release](https://github.com/semantic-release/semantic-release). You can see it on the
5 | [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 contributors and maintainers
6 | pledge to making participation in our project and our community a harassment-free experience for
7 | everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity
8 | and expression, level of experience, education, socio-economic status, nationality, personal
9 | appearance, race, religion, or sexual identity and orientation.
10 |
11 | ## Our Standards
12 |
13 | Examples of behavior that contributes to creating a positive environment include:
14 |
15 | - Using welcoming and inclusive language
16 | - Being respectful of differing viewpoints and experiences
17 | - Gracefully accepting constructive criticism
18 | - Focusing on what is best for the community
19 | - Showing empathy towards other community members
20 |
21 | Examples of unacceptable behavior by participants include:
22 |
23 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
24 | - Trolling, insulting/derogatory comments, and personal or political attacks
25 | - Public or private harassment
26 | - Publishing others' private information, such as a physical or electronic address, without explicit
27 | permission
28 | - Other conduct which could reasonably be considered inappropriate in a professional setting
29 |
30 | ## Our Responsibilities
31 |
32 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are
33 | expected to take appropriate and fair corrective action in response to any instances of unacceptable
34 | behavior.
35 |
36 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits,
37 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or
38 | to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate,
39 | threatening, offensive, or harmful.
40 |
41 | ## Scope
42 |
43 | This Code of Conduct applies both within project spaces and in public spaces when an individual is
44 | representing the project or its community. Examples of representing a project or community include
45 | using an official project e-mail address, posting via an official social media account, or acting as
46 | an appointed representative at an online or offline event. Representation of a project may be
47 | further defined and clarified by project maintainers.
48 |
49 | ## Enforcement
50 |
51 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting
52 | the project team at brandonvcarroll@gmail.com. All complaints will be reviewed and investigated and
53 | will result in a response that is deemed necessary and appropriate to the circumstances. The project
54 | team is obligated to maintain confidentiality with regard to the reporter of an incident. Further
55 | details of specific enforcement policies may be posted separately.
56 |
57 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face
58 | temporary or permanent repercussions as determined by other members of the project's leadership.
59 |
60 | ## Attribution
61 |
62 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
63 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
64 |
65 | [homepage]: https://www.contributor-covenant.org
66 |
67 | For answers to common questions about this code of conduct, see
68 | https://www.contributor-covenant.org/faq
69 |
--------------------------------------------------------------------------------
/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 | > [!CAUTION]
2 | > **This package is deprecated and is no longer actively maintained.**
3 | >
4 | > We encourage you to migrate to React Native Testing Library, v12.4 or later, which includes modern [built-in Jest matchers](https://callstack.github.io/react-native-testing-library/docs/api/jest-matchers) based on the matchers for this repository.
5 | >
6 | > The migration process should be relatively straightforward, we have a [migration guide](https://callstack.github.io/react-native-testing-library/docs/migration/jest-matchers) available.
7 |
8 |
9 |
10 |
jest-native
11 |
12 |
13 |
19 |
20 |
21 |
Custom jest matchers to test the state of React Native.
22 |
23 |
24 |
25 |
26 | [](https://www.npmjs.com/package/@testing-library/jest-native)
27 | [](https://codecov.io/github/testing-library/jest-native)
28 | 
29 | [](http://www.npmtrends.com/@testing-library/jest-native)
30 | [](http://makeapullrequest.com)
31 | [](https://discord.gg/testing-library)
32 | [](https://github.com/testing-library/jest-native/stargazers)
33 |
34 |
35 |
36 |
37 | ## Table of Contents
38 |
39 | - [The problem](#the-problem)
40 | - [This solution](#this-solution)
41 | - [Compatibility](#compatibility)
42 | - [Installation](#installation)
43 | - [Usage](#usage)
44 | - [Extending Jest matchers](#extending-jest-matchers)
45 | - [TypeScript support](#typescript-support)
46 | - [Matchers](#matchers)
47 | - [`toBeDisabled`](#tobedisabled)
48 | - [`toBeEnabled`](#tobeenabled)
49 | - [`toBeEmptyElement`](#tobeemptyelement)
50 | - [`toContainElement`](#tocontainelement)
51 | - [`toBeOnTheScreen`](#tobeonthescreen)
52 | - [`toHaveProp`](#tohaveprop)
53 | - [`toHaveTextContent`](#tohavetextcontent)
54 | - [`toHaveStyle`](#tohavestyle)
55 | - [`toBeVisible`](#tobevisible)
56 | - [`toHaveAccessibilityState`](#tohaveaccessibilitystate)
57 | - [`toHaveAccessibilityValue`](#tohaveaccessibilityvalue)
58 | - [Inspiration](#inspiration)
59 | - [Other solutions](#other-solutions)
60 | - [Contributors](#contributors)
61 |
62 |
63 |
64 | ## The problem
65 |
66 | You want to use [jest](https://facebook.github.io/jest/) to write tests that assert various things
67 | about the state of a React Native app. As part of that goal, you want to avoid all the repetitive
68 | patterns that arise in doing so like checking for a native element's props, its text content, its
69 | styles, and more.
70 |
71 | ## This solution
72 |
73 | The `jest-native` library provides a set of custom jest matchers that you can use to extend jest.
74 | These will make your tests more declarative, clear to read and to maintain.
75 |
76 | ## Compatibility
77 |
78 | These matchers should, for the most part, be agnostic enough to work with any React Native testing
79 | utilities, but they are primarily intended to be used with
80 | [React Native Testing Library](https://github.com/callstack/react-native-testing-library). Any
81 | issues raised with existing matchers or any newly proposed matchers must be viewed through
82 | compatibility with that library and its guiding principles first.
83 |
84 | ## Installation
85 |
86 | This module should be installed as one of your project's `devDependencies`:
87 |
88 | #### Using `yarn`
89 |
90 | ```sh
91 | yarn add --dev @testing-library/jest-native
92 | ```
93 |
94 | #### Using `npm`
95 |
96 | ```sh
97 | npm install --save-dev @testing-library/jest-native
98 | ```
99 |
100 | You will need `react-test-renderer`, `react`, and `react-native` installed in order to use this
101 | package.
102 |
103 | ## Usage
104 |
105 | ### Extending Jest matchers
106 |
107 | Import `@testing-library/jest-native/extend-expect` once (for instance in your
108 | [tests setup file](https://jestjs.io/docs/configuration#setupfilesafterenv-array)) and you're good
109 | to go:
110 |
111 | ```javascript
112 | import '@testing-library/jest-native/extend-expect';
113 | ```
114 |
115 | Alternatively, you can selectively import only the matchers you intend to use, and extend jest's
116 | `expect` yourself:
117 |
118 | ```javascript
119 | import { toBeEmptyElement, toHaveTextContent } from '@testing-library/jest-native';
120 |
121 | expect.extend({ toBeEmptyElement, toHaveTextContent });
122 | ```
123 |
124 | ### TypeScript support
125 |
126 | In order to setup proper TypeScript type checking use either one of the following approches.
127 |
128 | #### 1. Use TypeScript Jest setup file.
129 |
130 | Use `jest-setup.ts` file (instead of `jest-setup.js` file) which is added to Jest config's `setupFilesAfterEnv` option.
131 |
132 | The Jest setup file should contain following line:
133 |
134 | ```typescript
135 | import '@testing-library/jest-native/extend-expect';
136 | ```
137 |
138 | This should enable TypeScript checkign for both `tsc` and VS Code intellisense.
139 |
140 | #### 2. Use `declarations.d.ts` file
141 |
142 | Alternatively, create `declarations.d.ts` file at the root level of your project, if it does not exist already.
143 |
144 | Add following line at the top of your `declarations.d.ts`:
145 |
146 | ```
147 | ///
148 | ```
149 |
150 | This should enable TypeScript checkign for both `tsc` and VS Code intellisense.
151 |
152 | ## Matchers
153 |
154 | `jest-native` has only been tested to work with
155 | [React Native Testing Library](https://github.com/callstack/react-native-testing-library). Keep in
156 | mind that these queries are intended only to work with elements corresponding to
157 | [host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components).
158 |
159 | ### `toBeDisabled`
160 |
161 | ```javascript
162 | toBeDisabled();
163 | ```
164 |
165 | Check whether or not an element is disabled from a user perspective.
166 |
167 | This matcher will check if the element or its parent has any of the following props :
168 |
169 | - `disabled`
170 | - `accessibilityState={{ disabled: true }}`
171 | - `editable={false}` (for `TextInput` only)
172 |
173 | #### Examples
174 |
175 | ```javascript
176 | const { getByTestId } = render(
177 |
178 | ,
181 | );
182 |
183 | expect(getByTestId('button')).toBeDisabled();
184 | expect(getByTestId('input')).toBeDisabled();
185 | ```
186 |
187 | ### `toBeEnabled`
188 |
189 | ```javascript
190 | toBeEnabled();
191 | ```
192 |
193 | Check whether or not an element is enabled from a user perspective.
194 |
195 | Works similarly to `expect().not.toBeDisabled()`.
196 |
197 | #### Examples
198 |
199 | ```javascript
200 | const { getByTestId } = render(
201 |
202 | ,
205 | );
206 |
207 | expect(getByTestId('button')).toBeEnabled();
208 | expect(getByTestId('input')).toBeEnabled();
209 | ```
210 |
211 | ### `toBeEmptyElement`
212 |
213 | ```javascript
214 | toBeEmptyElement();
215 | ```
216 |
217 | Check that the given element has no content.
218 |
219 | #### Examples
220 |
221 | ```javascript
222 | const { getByTestId } = render();
223 |
224 | expect(getByTestId('empty')).toBeEmptyElement();
225 | ```
226 |
227 | > **Note**
This matcher has been previously named `toBeEmpty()`, but we changed that name in
228 | > order to avoid conflict with Jest Extendend matcher with the
229 | > [same name](https://github.com/jest-community/jest-extended#tobeempty).
230 |
231 | ### `toContainElement`
232 |
233 | ```typescript
234 | toContainElement(element: ReactTestInstance | null);
235 | ```
236 |
237 | Check if an element contains another element as a descendant. Again, will only work for native
238 | elements.
239 |
240 | #### Examples
241 |
242 | ```javascript
243 | const { queryByTestId } = render(
244 |
245 |
246 |
247 |
248 |
249 | ,
250 | );
251 |
252 | const grandparent = queryByTestId('grandparent');
253 | const parent = queryByTestId('parent');
254 | const child = queryByTestId('child');
255 | const textElement = queryByTestId('text-element');
256 |
257 | expect(grandparent).toContainElement(parent);
258 | expect(grandparent).toContainElement(child);
259 | expect(grandparent).toContainElement(textElement);
260 | expect(parent).toContainElement(child);
261 | expect(parent).not.toContainElement(grandparent);
262 | ```
263 |
264 | ### `toBeOnTheScreen`
265 |
266 | ```ts
267 | toBeOnTheScreen();
268 | ```
269 |
270 | Check that the element is present in the element tree.
271 |
272 | You can check that an already captured element has not been removed from the element tree.
273 |
274 | > **Note**
This matcher requires React Native Testing Library v10.1 or later, as it includes
275 | > the `screen` object.
276 | >
277 | > **Note**
If you're using React Native Testing Library v12 or later, you need to install Jest
278 | > Native v5.4.2 or later.
279 |
280 | #### Examples
281 |
282 | ```tsx
283 | render(
284 |
285 |
286 | ,
287 | );
288 |
289 | const child = screen.getByTestId('child');
290 | expect(child).toBeOnTheScreen();
291 |
292 | screen.update();
293 | expect(child).not.toBeOnTheScreen();
294 | ```
295 |
296 | ### `toHaveProp`
297 |
298 | ```typescript
299 | toHaveProp(prop: string, value?: any);
300 | ```
301 |
302 | Check that the element has a given prop.
303 |
304 | You can optionally check that the attribute has a specific expected value.
305 |
306 | #### Examples
307 |
308 | ```javascript
309 | const { queryByTestId } = render(
310 |
311 |
312 | text
313 |
314 |
315 | ,
316 | );
317 |
318 | expect(queryByTestId('button')).toHaveProp('accessible');
319 | expect(queryByTestId('button')).not.toHaveProp('disabled');
320 | expect(queryByTestId('button')).not.toHaveProp('title', 'ok');
321 | ```
322 |
323 | ### `toHaveTextContent`
324 |
325 | ```typescript
326 | toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean });
327 | ```
328 |
329 | Check if an element or its children have the supplied text.
330 |
331 | This will perform a partial, case-sensitive match when a string match is provided. To perform a
332 | case-insensitive match, you can use a `RegExp` with the `/i` modifier.
333 |
334 | To enforce matching the complete text content, pass a `RegExp`.
335 |
336 | #### Examples
337 |
338 | ```javascript
339 | const { queryByTestId } = render(2);
340 |
341 | expect(queryByTestId('count-value')).toHaveTextContent('2');
342 | expect(queryByTestId('count-value')).toHaveTextContent(2);
343 | expect(queryByTestId('count-value')).toHaveTextContent(/2/);
344 | expect(queryByTestId('count-value')).not.toHaveTextContent('21');
345 | ```
346 |
347 | ### `toHaveStyle`
348 |
349 | ```typescript
350 | toHaveStyle(style: object[] | object);
351 | ```
352 |
353 | Check if an element has the supplied styles.
354 |
355 | You can pass either an object of React Native style properties, or an array of objects with style
356 | properties. You cannot pass properties from a React Native stylesheet.
357 |
358 | #### Examples
359 |
360 | ```javascript
361 | const styles = StyleSheet.create({ text: { fontSize: 16 } });
362 |
363 | const { queryByText } = render(
364 |
370 | Hello World
371 | ,
372 | );
373 |
374 | expect(getByText('Hello World')).toHaveStyle({ color: 'black' });
375 | expect(getByText('Hello World')).toHaveStyle({ fontWeight: '600' });
376 | expect(getByText('Hello World')).toHaveStyle({ fontSize: 16 });
377 | expect(getByText('Hello World')).toHaveStyle([{ fontWeight: '600' }, { color: 'black' }]);
378 | expect(getByText('Hello World')).toHaveStyle({ color: 'black', fontWeight: '600', fontSize: 16 });
379 | expect(getByText('Hello World')).toHaveStyle({ transform: [{ scale: 2 }, { rotate: '45deg' }] });
380 | expect(getByText('Hello World')).not.toHaveStyle({ color: 'white' });
381 | expect(getByText('Hello World')).not.toHaveStyle({ transform: [{ scale: 2 }] });
382 | expect(getByText('Hello World')).not.toHaveStyle({
383 | transform: [{ rotate: '45deg' }, { scale: 2 }],
384 | });
385 | ```
386 |
387 | ### `toBeVisible`
388 |
389 | ```typescript
390 | toBeVisible();
391 | ```
392 |
393 | Check that the given element is visible to the user.
394 |
395 | An element is visible if **all** the following conditions are met:
396 |
397 | - it does not have its style property `display` set to `none`.
398 | - it does not have its style property `opacity` set to `0`.
399 | - it is not a `Modal` component or it does not have the prop `visible` set to `false`.
400 | - it is not hidden from accessibility as checked by
401 | [`isHiddenFromAccessibility`](https://callstack.github.io/react-native-testing-library/docs/api/#ishiddenfromaccessibility)
402 | function from React Native Testing Library
403 | - its ancestor elements are also visible.
404 |
405 | #### Examples
406 |
407 | ```javascript
408 | const { getByTestId } = render();
409 |
410 | expect(getByTestId('empty-view')).toBeVisible();
411 | ```
412 |
413 | ```javascript
414 | const { getByTestId } = render();
415 |
416 | expect(getByTestId('view-with-opacity')).toBeVisible();
417 | ```
418 |
419 | ```javascript
420 | const { getByTestId } = render();
421 |
422 | expect(getByTestId('empty-modal')).toBeVisible();
423 | ```
424 |
425 | ```javascript
426 | const { getByTestId } = render(
427 |
428 |
429 |
430 |
431 | ,
432 | );
433 |
434 | expect(getByTestId('view-within-modal')).toBeVisible();
435 | ```
436 |
437 | ```javascript
438 | const { getByTestId } = render();
439 |
440 | expect(getByTestId('invisible-view')).not.toBeVisible();
441 | ```
442 |
443 | ```javascript
444 | const { getByTestId } = render();
445 |
446 | expect(getByTestId('display-none-view')).not.toBeVisible();
447 | ```
448 |
449 | ```javascript
450 | const { getByTestId } = render(
451 |
452 |
453 |
454 |
455 | ,
456 | );
457 |
458 | expect(getByTestId('view-within-invisible-view')).not.toBeVisible();
459 | ```
460 |
461 | ```javascript
462 | const { getByTestId } = render(
463 |
464 |
465 |
466 |
467 | ,
468 | );
469 |
470 | expect(getByTestId('view-within-display-none-view')).not.toBeVisible();
471 | ```
472 |
473 | ```javascript
474 | const { getByTestId } = render(
475 |
476 |
477 |
478 |
479 | ,
480 | );
481 |
482 | // Children elements of not visible modals are not rendered.
483 | expect(queryByTestId('view-within-modal')).toBeNull();
484 | ```
485 |
486 | ```javascript
487 | const { getByTestId } = render();
488 |
489 | expect(getByTestId('not-visible-modal')).not.toBeVisible();
490 | ```
491 |
492 | ```javascript
493 | const { getByTestId } = render();
494 |
495 | expect(getByTestId('test')).not.toBeVisible();
496 | ```
497 |
498 | ```javascript
499 | const { getByTestId } = render(
500 | ,
501 | );
502 |
503 | expect(getByTestId('test')).not.toBeVisible();
504 | ```
505 |
506 | ### `toHaveAccessibilityState`
507 |
508 | ```ts
509 | toHaveAccessibilityState(state: {
510 | disabled?: boolean;
511 | selected?: boolean;
512 | checked?: boolean | 'mixed';
513 | busy?: boolean;
514 | expanded?: boolean;
515 | });
516 | ```
517 |
518 | Check that the element has given accessibility state entries.
519 |
520 | This check is based on `accessibilityState` prop but also takes into account the default entries
521 | which have been found by experimenting with accessibility inspector and screen readers on both iOS
522 | and Android.
523 |
524 | Some state entries behave as if explicit `false` value is the same as not having given state entry,
525 | so their default value is `false`:
526 |
527 | - `disabled`
528 | - `selected`
529 | - `busy`
530 |
531 | The remaining state entries behave as if explicit `false` value is different than not having given
532 | state entry, so their default value is `undefined`:
533 |
534 | - `checked`
535 | - `expanded`
536 |
537 | This matcher is compatible with `*ByRole` and `*ByA11State` queries from React Native Testing
538 | Library.
539 |
540 | #### Examples
541 |
542 | ```js
543 | render();
544 |
545 | // Single value match
546 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ expanded: true });
547 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ checked: true });
548 |
549 | // Can match multiple entries
550 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ expanded: true, checked: true });
551 | ```
552 |
553 | Default values handling:
554 |
555 | ```js
556 | render();
557 |
558 | // Matching states where default value is `false`
559 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ disabled: false });
560 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ selected: false });
561 | expect(screen.getByTestId('view')).toHaveAccessibilityState({ busy: false });
562 |
563 | // Matching states where default value is `undefined`
564 | expect(screen.getByTestId('view')).not.toHaveAccessibilityState({ checked: false });
565 | expect(screen.getByTestId('view')).not.toHaveAccessibilityState({ expanded: false });
566 | ```
567 |
568 | ### `toHaveAccessibilityValue`
569 |
570 | ```ts
571 | toHaveAccessibilityValue(value: {
572 | min?: number;
573 | max?: number;
574 | now?: number;
575 | text?: string | RegExp;
576 | });
577 | ```
578 |
579 | Check that the element has given `accessibilityValue` prop entries.
580 |
581 | This matcher is compatible with `*ByRole` and `*ByA11Value` queries from React Native Testing
582 | Library.
583 |
584 | #### Examples
585 |
586 | ```js
587 | render();
588 |
589 | const view = screen.getByTestId('view');
590 |
591 | // Single value match
592 | expect(view).toHaveAccessibilityValue({ now: 65 });
593 | expect(view).toHaveAccessibilityValue({ max: 0 });
594 |
595 | // Can match multiple entries
596 | expect(view).toHaveAccessibilityValue({ min: 0, max: 100 });
597 | expect(view).toHaveAccessibilityValue({ min: 0, max: 100, now: 65 });
598 |
599 | // All specified entries need to match
600 | expect(view).not.toHaveAccessibilityValue({ now: 45 });
601 | expect(view).not.toHaveAccessibilityValue({ min: 20, max: 100, now: 65 });
602 | ```
603 |
604 | ```js
605 | render();
606 |
607 | const view = screen.getByTestId('view');
608 | expect(view).toHaveAccessibilityValue({ text: 'Almost full' });
609 | expect(view).toHaveAccessibilityValue({ text: /full/ });
610 | ```
611 |
612 | ## Inspiration
613 |
614 | This library was made to be a companion for
615 | [React Native Testing Library](https://github.com/callstack/react-native-testing-library).
616 |
617 | It was inspired by [jest-dom](https://github.com/gnapse/jest-dom/), the companion library for
618 | [DTL](https://github.com/kentcdodds/dom-testing-library/). We emulated as many of those helpers as
619 | we could while keeping in mind the guiding principles.
620 |
621 | ## Other solutions
622 |
623 | None known, [you can add the first](http://makeapullrequest.com)!
624 |
625 | ## Contributors
626 |
627 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
628 |
629 |
630 |
631 |
632 |
643 |
644 |
645 |
646 |
647 |
648 |
649 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
650 | specification. Contributions of any kind welcome!
651 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | overrides: [
4 | {
5 | compact: false,
6 | },
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/extend-expect.d.ts:
--------------------------------------------------------------------------------
1 | import type { AccessibilityState, ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 | import type { AccessibilityValueMatcher } from './src/to-have-accessibility-value';
4 |
5 | export interface JestNativeMatchers {
6 | toBeDisabled(): R;
7 | toBeEmptyElement(): R;
8 | toBeEnabled(): R;
9 | toBeOnTheScreen(): R;
10 | toBeVisible(): R;
11 | toContainElement(element: ReactTestInstance | null): R;
12 | toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean }): R;
13 | toHaveProp(attr: string, value?: unknown): R;
14 | toHaveStyle(style: StyleProp): R;
15 | toHaveAccessibilityState(state: AccessibilityState): R;
16 | toHaveAccessibilityValue(value: AccessibilityValueMatcher): R;
17 |
18 | /** @deprecated This function has been renamed to `toBeEmptyElement`. */
19 | toBeEmpty(): R;
20 | }
21 |
22 | // Implicit Jest global `expect`.
23 | declare global {
24 | namespace jest {
25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
26 | interface Matchers extends JestNativeMatchers {}
27 | }
28 | }
29 |
30 | // Explicit `@jest/globals` `expect` matchers.
31 | declare module '@jest/expect' {
32 | interface Matchers> extends JestNativeMatchers {}
33 | }
34 |
--------------------------------------------------------------------------------
/extend-expect.js:
--------------------------------------------------------------------------------
1 | require('./dist/extend-expect');
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@testing-library/react-native',
3 | testMatch: ['**/__tests__/**/*.+(js|jsx|ts|tsx)'],
4 | setupFilesAfterEnv: ['/setup-tests.ts'],
5 | snapshotSerializers: ['@relmify/jest-serializer-strip-ansi/always'],
6 | collectCoverageFrom: ['src/**/*.(js|jsx|ts|tsx)', '!src/**/*.test-d.(ts|tsx)'],
7 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/helpers/', '/dist/', '__mocks__'],
8 | };
9 |
--------------------------------------------------------------------------------
/legacy-extend-expect.d.ts:
--------------------------------------------------------------------------------
1 | import type { AccessibilityState, ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 | import type { AccessibilityValueMatcher } from './src/to-have-accessibility-value';
4 |
5 | export interface JestNativeMatchers {
6 | legacy_toBeDisabled(): R;
7 | legacy_toBeEmptyElement(): R;
8 | legacy_toBeEnabled(): R;
9 | legacy_toBeOnTheScreen(): R;
10 | legacy_toBeVisible(): R;
11 | legacy_toContainElement(element: ReactTestInstance | null): R;
12 | legacy_toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean }): R;
13 | legacy_toHaveProp(attr: string, value?: unknown): R;
14 | legacy_toHaveStyle(style: StyleProp): R;
15 | legacy_toHaveAccessibilityState(state: AccessibilityState): R;
16 | legacy_toHaveAccessibilityValue(value: AccessibilityValueMatcher): R;
17 | }
18 |
19 | // Implicit Jest global `expect`.
20 | declare global {
21 | namespace jest {
22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
23 | interface Matchers extends JestNativeMatchers {}
24 | }
25 | }
26 |
27 | // Explicit `@jest/globals` `expect` matchers.
28 | declare module '@jest/expect' {
29 | interface Matchers> extends JestNativeMatchers {}
30 | }
31 |
--------------------------------------------------------------------------------
/legacy-extend-expect.js:
--------------------------------------------------------------------------------
1 | require('./dist/legacy-extend-expect');
2 |
--------------------------------------------------------------------------------
/other/eagle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/jest-native/47fc1ab54afdf625f74e47db7e4dda42ec7fb62a/other/eagle.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@testing-library/jest-native",
3 | "version": "0.0.0-semantically-released",
4 | "description": "Custom jest matchers to test the state of React Native",
5 | "main": "dist/index.js",
6 | "types": "extend-expect.d.ts",
7 | "scripts": {
8 | "prepare": "husky install",
9 | "commit": "git-cz",
10 | "commit:add": "git add .",
11 | "commit:all": "npm run commit:add && npm run commit",
12 | "readme:toc": "doctoc README.md --maxlevel 3 --title '## Table of Contents'",
13 | "test": "jest --colors",
14 | "lint": "eslint .",
15 | "prepublishOnly": "rm -rf dist && tsc -p tsconfig.prod.json",
16 | "semantic-release": "semantic-release",
17 | "test:coverage": "jest --coverage --colors",
18 | "test:watch": "jest --watch --coverage",
19 | "typecheck": "tsc -noEmit"
20 | },
21 | "files": [
22 | "dist",
23 | "extend-expect.js",
24 | "extend-expect.d.ts",
25 | "legacy-extend-expect.js",
26 | "legacy-extend-expect.d.ts"
27 | ],
28 | "keywords": [
29 | "testing",
30 | "testing-library",
31 | "jest",
32 | "react-native"
33 | ],
34 | "dependencies": {
35 | "chalk": "^4.1.2",
36 | "jest-diff": "^29.0.1",
37 | "jest-matcher-utils": "^29.0.1",
38 | "pretty-format": "^29.0.3",
39 | "redent": "^3.0.0"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.18.10",
43 | "@babel/core": "^7.18.10",
44 | "@babel/runtime": "^7.18.9",
45 | "@callstack/eslint-config": "^13.0.1",
46 | "@relmify/jest-serializer-strip-ansi": "^1.0.2",
47 | "@testing-library/react-native": "^12.0.0-rc.0",
48 | "@types/jest": "^29.0.1",
49 | "@types/react": "^18.0.19",
50 | "@types/react-native": "^0.71.0",
51 | "@types/react-test-renderer": "^18.0.0",
52 | "commitizen": "^4.2.5",
53 | "cz-conventional-changelog": "^3.3.0",
54 | "eslint": "^8.21.0",
55 | "husky": "^8.0.1",
56 | "jest": "^29.0.1",
57 | "metro-react-native-babel-preset": "0.73.7",
58 | "prettier": "^2.7.1",
59 | "pretty-quick": "^3.1.3",
60 | "react": "18.2.0",
61 | "react-native": "0.71.8",
62 | "react-test-renderer": "18.2.0",
63 | "semantic-release": "^19.0.3",
64 | "typescript": "^5.0.4"
65 | },
66 | "peerDependencies": {
67 | "react": ">=16.0.0",
68 | "react-native": ">=0.59",
69 | "react-test-renderer": ">=16.0.0"
70 | },
71 | "prettier": {
72 | "printWidth": 100,
73 | "singleQuote": true,
74 | "trailingComma": "all",
75 | "tabWidth": 2,
76 | "proseWrap": "always"
77 | },
78 | "eslintConfig": {
79 | "extends": "@callstack",
80 | "env": {
81 | "jest": true
82 | },
83 | "rules": {
84 | "react-native/no-color-literals": "off",
85 | "react-native/no-inline-styles": "off",
86 | "react-native/no-raw-text": "off",
87 | "react-native-a11y/has-accessibility-hint": "off",
88 | "react-native-a11y/has-valid-accessibility-descriptors": "off",
89 | "react-native-a11y/has-valid-accessibility-ignores-invert-colors": "off",
90 | "react-native-a11y/has-valid-accessibility-value": "off",
91 | "@typescript-eslint/consistent-type-imports": "error",
92 | "@typescript-eslint/no-explicit-any": "error"
93 | }
94 | },
95 | "eslintIgnore": [
96 | "node_modules/",
97 | "dist/",
98 | "lib/"
99 | ],
100 | "release": {
101 | "branches": [
102 | "main"
103 | ],
104 | "plugins": [
105 | [
106 | "@semantic-release/commit-analyzer",
107 | {
108 | "releaseRules": [
109 | {
110 | "type": "feature",
111 | "release": "minor"
112 | },
113 | {
114 | "type": "refactor",
115 | "release": "patch"
116 | }
117 | ]
118 | }
119 | ],
120 | "@semantic-release/release-notes-generator",
121 | "@semantic-release/npm",
122 | "@semantic-release/github"
123 | ]
124 | },
125 | "config": {
126 | "commitizen": {
127 | "path": "./node_modules/cz-conventional-changelog"
128 | }
129 | },
130 | "repository": {
131 | "type": "git",
132 | "url": "git+https://github.com/testing-library/jest-native.git"
133 | },
134 | "author": "Brandon Carroll",
135 | "license": "MIT",
136 | "bugs": {
137 | "url": "https://github.com/testing-library/jest-native/issues"
138 | },
139 | "homepage": "https://github.com/testing-library/jest-native#readme"
140 | }
141 |
--------------------------------------------------------------------------------
/setup-tests.ts:
--------------------------------------------------------------------------------
1 | import './src/extend-expect';
2 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/to-be-visible.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`.toBeVisible throws an error when expectation is not matched 1`] = `
4 | "expect(element).not.toBeVisible()
5 |
6 | Received element is visible:
7 | "
10 | `;
11 |
12 | exports[`.toBeVisible throws an error when expectation is not matched 2`] = `
13 | "expect(element).toBeVisible()
14 |
15 | Received element is not visible:
16 | "
24 | `;
25 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/to-have-style.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`.toHaveStyle handles negative test cases 1`] = `
4 | "expect(element).toHaveStyle()
5 |
6 | - Expected
7 |
8 | backgroundColor: blue;
9 | transform: [
10 | {
11 | - "scale": 1
12 | + "scale": 2
13 | + },
14 | + {
15 | + "rotate": "45deg"
16 | }
17 | ];"
18 | `;
19 |
20 | exports[`.toHaveStyle handles transform when transform undefined 1`] = `
21 | "expect(element).toHaveStyle()
22 |
23 | - Expected
24 |
25 | - transform: [
26 | - {
27 | - "scale": 1
28 | - }
29 | - ];
30 | + transform: undefined;"
31 | `;
32 |
--------------------------------------------------------------------------------
/src/__tests__/component-tree.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 | import { getParentElement } from '../component-tree';
5 |
6 | function MultipleHostChildren() {
7 | return (
8 | <>
9 |
10 |
11 |
12 | >
13 | );
14 | }
15 |
16 | describe('getParentElement()', () => {
17 | it('returns host parent for host component', () => {
18 | const view = render(
19 |
20 |
21 |
22 |
23 |
24 | ,
25 | );
26 |
27 | const hostParent = getParentElement(view.getByTestId('subject'));
28 | expect(hostParent).toBe(view.getByTestId('parent'));
29 |
30 | const hostGrandparent = getParentElement(hostParent);
31 | expect(hostGrandparent).toBe(view.getByTestId('grandparent'));
32 |
33 | expect(getParentElement(hostGrandparent)).toBe(null);
34 | });
35 |
36 | it('returns host parent for null', () => {
37 | expect(getParentElement(null)).toBe(null);
38 | });
39 |
40 | it('returns host parent for composite component', () => {
41 | const view = render(
42 |
43 |
44 |
45 | ,
46 | );
47 |
48 | const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
49 | const hostParent = getParentElement(compositeComponent);
50 | expect(hostParent).toBe(view.getByTestId('parent'));
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/src/__tests__/legacy-extend-expect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text } from 'react-native';
3 | import { render, screen } from '@testing-library/react-native';
4 | import '../legacy-extend-expect';
5 |
6 | test('legacy expect.extend() works correctly', () => {
7 | render(
8 |
9 | Hello
10 | ,
11 | );
12 | expect(screen.getByTestId('view')).legacy_toBeOnTheScreen();
13 | expect(screen.getByTestId('view')).legacy_toHaveTextContent('Hello');
14 | expect(screen.getByTestId('view')).legacy_toBeVisible();
15 | expect(screen.getByTestId('view')).legacy_toBeEnabled();
16 | expect(screen.getByTestId('view')).not.legacy_toBeDisabled();
17 | });
18 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-disabled.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | TouchableHighlight,
5 | TouchableOpacity,
6 | TouchableWithoutFeedback,
7 | TouchableNativeFeedback,
8 | View,
9 | TextInput,
10 | Pressable,
11 | } from 'react-native';
12 | import { render } from '@testing-library/react-native';
13 |
14 | const ALLOWED_COMPONENTS = {
15 | View,
16 | TextInput,
17 | TouchableHighlight,
18 | TouchableOpacity,
19 | TouchableWithoutFeedback,
20 | TouchableNativeFeedback,
21 | Pressable,
22 | };
23 |
24 | describe('.toBeDisabled', () => {
25 | Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => {
26 | test(`handle disabled prop for element ${name}`, () => {
27 | const { queryByTestId } = render(
28 | //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604)
29 |
30 |
31 | ,
32 | );
33 |
34 | expect(queryByTestId(name)).toBeDisabled();
35 | expect(() => expect(queryByTestId(name)).not.toBeDisabled()).toThrow();
36 | });
37 | });
38 |
39 | Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => {
40 | test(`handle disabled in accessibilityState for element ${name}`, () => {
41 | const { queryByTestId } = render(
42 | //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604)
43 |
44 |
45 | ,
46 | );
47 |
48 | expect(queryByTestId(name)).toBeDisabled();
49 | expect(() => expect(queryByTestId(name)).not.toBeDisabled()).toThrow();
50 | });
51 | });
52 |
53 | test('handle editable prop for TextInput', () => {
54 | const { getByTestId, getByPlaceholderText } = render(
55 |
56 |
57 |
58 |
59 | ,
60 | );
61 |
62 | // Check host TextInput
63 | expect(getByTestId('disabled')).toBeDisabled();
64 | expect(getByTestId('enabled-by-default')).not.toBeDisabled();
65 | expect(getByTestId('enabled')).not.toBeDisabled();
66 |
67 | // Check composite TextInput
68 | expect(getByPlaceholderText('disabled')).toBeDisabled();
69 | expect(getByPlaceholderText('enabled-by-default')).not.toBeDisabled();
70 | expect(getByPlaceholderText('enabled')).not.toBeDisabled();
71 | });
72 | });
73 |
74 | describe('.toBeEnabled', () => {
75 | Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => {
76 | test(`handle disabled prop for element ${name} when undefined`, () => {
77 | const { queryByTestId } = render(
78 | //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604)
79 |
80 |
81 | ,
82 | );
83 |
84 | expect(queryByTestId(name)).toBeEnabled();
85 | expect(() => expect(queryByTestId(name)).not.toBeEnabled()).toThrow();
86 | });
87 | });
88 |
89 | Object.entries(ALLOWED_COMPONENTS).forEach(([name, Component]) => {
90 | test(`handle disabled in accessibilityState for element ${name} when false`, () => {
91 | const { queryByTestId } = render(
92 | //@ts-expect-error JSX element type 'Component' does not have any construct or call signatures.ts(2604)
93 |
94 |
95 | ,
96 | );
97 |
98 | expect(queryByTestId(name)).toBeEnabled();
99 | expect(() => expect(queryByTestId(name)).not.toBeEnabled()).toThrow();
100 | });
101 | });
102 |
103 | test('handle editable prop for TextInput', () => {
104 | const { getByTestId, getByPlaceholderText } = render(
105 |
106 |
107 |
108 |
109 | ,
110 | );
111 |
112 | // Check host TextInput
113 | expect(getByTestId('enabled-by-default')).toBeEnabled();
114 | expect(getByTestId('enabled')).toBeEnabled();
115 | expect(getByTestId('disabled')).not.toBeEnabled();
116 |
117 | // Check composite TextInput
118 | expect(getByPlaceholderText('enabled-by-default')).toBeEnabled();
119 | expect(getByPlaceholderText('enabled')).toBeEnabled();
120 | expect(getByPlaceholderText('disabled')).not.toBeEnabled();
121 | });
122 | });
123 |
124 | describe('for .toBeEnabled/Disabled Button', () => {
125 | test('handles disabled prop for button', () => {
126 | const { queryByTestId } = render(
127 |
128 |
129 |
130 | ,
131 | );
132 |
133 | expect(queryByTestId('enabled')).toBeEnabled();
134 | expect(queryByTestId('disabled')).toBeDisabled();
135 | });
136 |
137 | test('handles button a11y state', () => {
138 | const { queryByTestId } = render(
139 |
140 |
141 |
142 | ,
143 | );
144 |
145 | expect(queryByTestId('enabled')).toBeEnabled();
146 | expect(queryByTestId('disabled')).toBeDisabled();
147 | });
148 |
149 | test('Errors when matcher misses', () => {
150 | const { queryByTestId, queryByText } = render(
151 |
152 |
153 |
154 | ,
155 | );
156 |
157 | expect(() => expect(queryByTestId('enabled')).toBeDisabled()).toThrow();
158 | expect(() => expect(queryByText('disabled')).toBeEnabled()).toThrow();
159 | });
160 | });
161 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-empty-element.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | test('.toBeEmptyElement', () => {
6 | const { queryByTestId } = render(
7 |
8 |
9 | ,
10 | );
11 |
12 | const empty = queryByTestId('empty');
13 | const notEmpty = queryByTestId('not-empty');
14 | const nonExistantElement = queryByTestId('not-exists');
15 | const fakeElement = { thisIsNot: 'an html element' };
16 |
17 | expect(empty).toBeEmptyElement();
18 | expect(notEmpty).not.toBeEmptyElement();
19 |
20 | // negative test cases wrapped in throwError assertions for coverage.
21 | expect(() => expect(empty).not.toBeEmptyElement()).toThrow();
22 |
23 | expect(() => expect(notEmpty).toBeEmptyElement()).toThrow();
24 |
25 | expect(() => expect(fakeElement).toBeEmptyElement()).toThrow();
26 |
27 | expect(() => {
28 | expect(nonExistantElement).toBeEmptyElement();
29 | }).toThrow();
30 | });
31 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-empty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | test('.toBeEmpty', () => {
6 | const { queryByTestId } = render(
7 |
8 |
9 | ,
10 | );
11 |
12 | const empty = queryByTestId('empty');
13 | const notEmpty = queryByTestId('not-empty');
14 | const nonExistantElement = queryByTestId('not-exists');
15 | const fakeElement = { thisIsNot: 'an html element' };
16 |
17 | expect(empty).toBeEmpty();
18 | expect(notEmpty).not.toBeEmpty();
19 |
20 | // negative test cases wrapped in throwError assertions for coverage.
21 | expect(() => expect(empty).not.toBeEmpty()).toThrow();
22 |
23 | expect(() => expect(notEmpty).toBeEmpty()).toThrow();
24 |
25 | expect(() => expect(fakeElement).toBeEmpty()).toThrow();
26 |
27 | expect(() => {
28 | expect(nonExistantElement).toBeEmpty();
29 | }).toThrow();
30 | });
31 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-on-the-screen-import.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | jest.mock('@testing-library/react-native', () => ({
6 | ...jest.requireActual('@testing-library/react-native'),
7 | screen: undefined,
8 | }));
9 |
10 | test('toBeOnTheScreen() on null element', () => {
11 | const screen = render();
12 |
13 | const test = screen.getByTestId('test');
14 | expect(() => expect(test).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
15 | "Could not import \`screen\` object from @testing-library/react-native.
16 |
17 | Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies."
18 | `);
19 | });
20 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-on-the-screen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View, Text } from 'react-native';
3 | import { render, screen } from '@testing-library/react-native';
4 |
5 | function ShowChildren({ show }: { show: boolean }) {
6 | return show ? (
7 |
8 | Hello
9 |
10 | ) : (
11 |
12 | );
13 | }
14 |
15 | test('toBeOnTheScreen() on attached element', () => {
16 | render();
17 | const element = screen.getByTestId('test');
18 | expect(element).toBeOnTheScreen();
19 | expect(() => expect(element).not.toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
20 | "expect(element).not.toBeOnTheScreen()
21 |
22 | expected element tree not to contain element but found:
23 | "
26 | `);
27 | });
28 |
29 | test('toBeOnTheScreen() on detached element', () => {
30 | render();
31 | const element = screen.getByTestId('text');
32 |
33 | screen.update();
34 | expect(element).toBeTruthy();
35 | expect(element).not.toBeOnTheScreen();
36 | expect(() => expect(element).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
37 | "expect(element).toBeOnTheScreen()
38 |
39 | element could not be found in the element tree"
40 | `);
41 | });
42 |
43 | test('toBeOnTheScreen() on null element', () => {
44 | expect(null).not.toBeOnTheScreen();
45 | expect(() => expect(null).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
46 | "expect(element).toBeOnTheScreen()
47 |
48 | element could not be found in the element tree"
49 | `);
50 | });
51 |
52 | test('example test', () => {
53 | render(
54 |
55 |
56 | ,
57 | );
58 |
59 | const child = screen.getByTestId('child');
60 | expect(child).toBeOnTheScreen();
61 |
62 | screen.update();
63 | expect(child).not.toBeOnTheScreen();
64 | });
65 |
--------------------------------------------------------------------------------
/src/__tests__/to-be-visible.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Pressable, Modal } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | describe('.toBeVisible', () => {
6 | test('handles empty view', () => {
7 | const { getByTestId } = render();
8 | expect(getByTestId('test')).toBeVisible();
9 | });
10 |
11 | test('handles view with opacity', () => {
12 | const { getByTestId } = render();
13 | expect(getByTestId('test')).toBeVisible();
14 | });
15 |
16 | test('handles view with 0 opacity', () => {
17 | const { getByTestId } = render();
18 | expect(getByTestId('test')).not.toBeVisible();
19 | });
20 |
21 | test('handles view with display "none"', () => {
22 | const { getByTestId } = render();
23 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
24 | });
25 |
26 | test('handles ancestor view with 0 opacity', () => {
27 | const { getByTestId } = render(
28 |
29 |
30 |
31 |
32 | ,
33 | );
34 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
35 | });
36 |
37 | test('handles ancestor view with display "none"', () => {
38 | const { getByTestId } = render(
39 |
40 |
41 |
42 |
43 | ,
44 | );
45 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
46 | });
47 |
48 | test('handles empty modal', () => {
49 | const { getByTestId } = render();
50 | expect(getByTestId('test')).toBeVisible();
51 | });
52 |
53 | test('handles view within modal', () => {
54 | const { getByTestId } = render(
55 |
56 |
57 |
58 |
59 | ,
60 | );
61 | expect(getByTestId('view-within-modal')).toBeVisible();
62 | });
63 |
64 | test('handles view within not visible modal', () => {
65 | const { getByTestId, queryByTestId } = render(
66 |
67 |
68 |
69 |
70 | ,
71 | );
72 | expect(getByTestId('test')).not.toBeVisible();
73 | // Children elements of not visible modals are not rendered.
74 | expect(() => expect(getByTestId('view-within-modal')).not.toBeVisible()).toThrow();
75 | expect(queryByTestId('view-within-modal')).toBeNull();
76 | });
77 |
78 | test('handles not visible modal', () => {
79 | const { getByTestId } = render();
80 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
81 | });
82 |
83 | test('handles inaccessible view (iOS)', () => {
84 | const { getByTestId, update } = render();
85 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
86 |
87 | update();
88 | expect(getByTestId('test')).toBeVisible();
89 | });
90 |
91 | test('handles view within inaccessible view (iOS)', () => {
92 | const { getByTestId } = render(
93 |
94 |
95 |
96 |
97 | ,
98 | );
99 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
100 | });
101 |
102 | test('handles inaccessible view (Android)', () => {
103 | const { getByTestId, update } = render(
104 | ,
105 | );
106 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
107 |
108 | update();
109 | expect(getByTestId('test')).toBeVisible();
110 | });
111 |
112 | test('handles view within inaccessible view (Android)', () => {
113 | const { getByTestId } = render(
114 |
115 |
116 |
117 |
118 | ,
119 | );
120 | expect(getByTestId('test', { includeHiddenElements: true })).not.toBeVisible();
121 | });
122 |
123 | test('handles null elements', () => {
124 | expect(() => expect(null).toBeVisible()).toThrowErrorMatchingInlineSnapshot(`
125 | "expect(received).toBeVisible()
126 |
127 | received value must be a React Element.
128 | Received has value: null"
129 | `);
130 | });
131 |
132 | test('handles non-React elements', () => {
133 | expect(() => expect({ name: 'Non-React element' }).not.toBeVisible()).toThrow();
134 | expect(() => expect(true).not.toBeVisible()).toThrow();
135 | });
136 |
137 | test('throws an error when expectation is not matched', () => {
138 | const { getByTestId, update } = render();
139 | expect(() => expect(getByTestId('test')).not.toBeVisible()).toThrowErrorMatchingSnapshot();
140 |
141 | update();
142 | expect(() => expect(getByTestId('test')).toBeVisible()).toThrowErrorMatchingSnapshot();
143 | });
144 |
145 | test('handles Pressable with function style prop', () => {
146 | const { getByTestId } = render(
147 | ({ backgroundColor: 'blue' })} />,
148 | );
149 | expect(getByTestId('test')).toBeVisible();
150 | });
151 | });
152 |
--------------------------------------------------------------------------------
/src/__tests__/to-contain-element.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | const { queryByTestId } = render(
6 |
7 |
8 |
9 |
10 |
11 | ,
12 | );
13 |
14 | const grandparent = queryByTestId('grandparent');
15 | const parent = queryByTestId('parent');
16 | const child = queryByTestId('child');
17 | const textElement = queryByTestId('text-element');
18 | const nonExistantElement = queryByTestId('not-exists');
19 | const fakeElement = undefined;
20 |
21 | test('.toContainElement positive test cases', () => {
22 | expect(grandparent).toContainElement(parent);
23 | expect(grandparent).toContainElement(child);
24 | expect(grandparent).toContainElement(textElement);
25 | expect(parent).toContainElement(child);
26 | expect(parent).not.toContainElement(grandparent);
27 | expect(parent).not.toContainElement(textElement);
28 | expect(child).not.toContainElement(parent);
29 | expect(child).not.toContainElement(grandparent);
30 | expect(child).not.toContainElement(textElement);
31 | expect(grandparent).not.toContainElement(nonExistantElement);
32 | expect(child).not.toContainElement(nonExistantElement);
33 | expect(parent).not.toContainElement(nonExistantElement);
34 | expect(textElement).not.toContainElement(grandparent);
35 | expect(textElement).not.toContainElement(parent);
36 | expect(textElement).not.toContainElement(child);
37 |
38 | // obscure cases
39 | expect(() => expect(child).toContainElement(null)).toThrow();
40 | expect(() => expect(parent).toContainElement(null)).toThrow();
41 | expect(() => expect(grandparent).toContainElement(null)).toThrow();
42 | });
43 |
44 | test('.toContainElement negative test cases', () => {
45 | expect(() => expect(nonExistantElement).not.toContainElement(child)).toThrow();
46 | expect(() => expect(parent).toContainElement(grandparent)).toThrow();
47 | expect(() => expect(nonExistantElement).toContainElement(grandparent)).toThrow();
48 | expect(() => expect(grandparent).toContainElement(nonExistantElement)).toThrow();
49 | expect(() => expect(nonExistantElement).toContainElement(nonExistantElement)).toThrow();
50 |
51 | // @ts-expect-error intentionally passing incorrect type
52 | expect(() => expect(nonExistantElement).toContainElement(fakeElement)).toThrow();
53 | expect(() => expect(fakeElement).toContainElement(nonExistantElement)).toThrow();
54 | expect(() => expect(fakeElement).not.toContainElement(nonExistantElement)).toThrow();
55 | expect(() => expect(fakeElement).toContainElement(grandparent)).toThrow();
56 |
57 | // @ts-expect-error intentionally passing incorrect type
58 | expect(() => expect(grandparent).toContainElement(fakeElement)).toThrow();
59 |
60 | // @ts-expect-error intentionally passing incorrect type
61 | expect(() => expect(fakeElement).toContainElement(fakeElement)).toThrow();
62 | expect(() => expect(grandparent).not.toContainElement(child)).toThrow();
63 | expect(() => expect(grandparent).not.toContainElement(textElement)).toThrow();
64 |
65 | // @ts-expect-error intentionally passing incorrect type
66 | expect(() => expect(grandparent).not.toContainElement(undefined)).toThrow();
67 | });
68 |
--------------------------------------------------------------------------------
/src/__tests__/to-have-accessibility-state.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | test('.toHaveAccessibilityState to handle explicit state', () => {
6 | const { getByTestId } = render(
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ,
19 | );
20 |
21 | expect(getByTestId('disabled')).toHaveAccessibilityState({ disabled: true });
22 | expect(getByTestId('disabled')).not.toHaveAccessibilityState({ disabled: false });
23 | expect(() => expect(getByTestId('disabled')).toHaveAccessibilityState({ disabled: false }))
24 | .toThrowErrorMatchingInlineSnapshot(`
25 | "expect(element).toHaveAccessibilityState({"disabled": false})
26 |
27 | Expected the element to have accessibility state:
28 | {"disabled": false}
29 | Received element with implied accessibility state:
30 | {"busy": false, "disabled": true, "selected": false}"
31 | `);
32 |
33 | expect(getByTestId('selected')).toHaveAccessibilityState({ selected: true });
34 | expect(getByTestId('selected')).not.toHaveAccessibilityState({ selected: false });
35 | expect(() => expect(getByTestId('selected')).not.toHaveAccessibilityState({ selected: true }))
36 | .toThrowErrorMatchingInlineSnapshot(`
37 | "expect(element).not.toHaveAccessibilityState({"selected": true})
38 |
39 | Expected the element not to have accessibility state:
40 | {"selected": true}
41 | Received element with implied accessibility state:
42 | {"busy": false, "disabled": false, "selected": true}"
43 | `);
44 |
45 | expect(getByTestId('busy')).toHaveAccessibilityState({ busy: true });
46 | expect(getByTestId('busy')).not.toHaveAccessibilityState({ busy: false });
47 |
48 | expect(getByTestId('checked-true')).toHaveAccessibilityState({ checked: true });
49 | expect(getByTestId('checked-true')).not.toHaveAccessibilityState({ checked: 'mixed' });
50 | expect(getByTestId('checked-true')).not.toHaveAccessibilityState({ checked: false });
51 |
52 | expect(getByTestId('checked-mixed')).toHaveAccessibilityState({ checked: 'mixed' });
53 | expect(getByTestId('checked-mixed')).not.toHaveAccessibilityState({ checked: true });
54 | expect(getByTestId('checked-mixed')).not.toHaveAccessibilityState({ checked: false });
55 |
56 | expect(getByTestId('checked-false')).toHaveAccessibilityState({ checked: false });
57 | expect(getByTestId('checked-false')).not.toHaveAccessibilityState({ checked: true });
58 | expect(getByTestId('checked-false')).not.toHaveAccessibilityState({ checked: 'mixed' });
59 |
60 | expect(getByTestId('expanded-true')).toHaveAccessibilityState({ expanded: true });
61 | expect(getByTestId('expanded-true')).not.toHaveAccessibilityState({ expanded: false });
62 |
63 | expect(getByTestId('expanded-false')).toHaveAccessibilityState({ expanded: false });
64 | expect(getByTestId('expanded-false')).not.toHaveAccessibilityState({ expanded: true });
65 |
66 | expect(getByTestId('disabled-selected')).toHaveAccessibilityState({
67 | disabled: true,
68 | selected: true,
69 | });
70 | expect(getByTestId('disabled-selected')).not.toHaveAccessibilityState({
71 | disabled: false,
72 | selected: true,
73 | });
74 | expect(getByTestId('disabled-selected')).not.toHaveAccessibilityState({
75 | disabled: true,
76 | selected: false,
77 | });
78 | });
79 |
80 | test('.toHaveAccessibilityState to handle implicit state', () => {
81 | const { getByTestId } = render();
82 |
83 | expect(getByTestId('subject')).toHaveAccessibilityState({ disabled: false });
84 | expect(getByTestId('subject')).toHaveAccessibilityState({ selected: false });
85 | expect(getByTestId('subject')).toHaveAccessibilityState({ busy: false });
86 |
87 | expect(getByTestId('subject')).not.toHaveAccessibilityState({ checked: false });
88 | expect(getByTestId('subject')).not.toHaveAccessibilityState({ expanded: false });
89 | });
90 |
--------------------------------------------------------------------------------
/src/__tests__/to-have-accessibility-value.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | test('.toHaveAccessibilityValue to handle min, max, now', () => {
6 | const { getByTestId } = render(
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ,
16 | );
17 |
18 | expect(getByTestId('min')).toHaveAccessibilityValue({ min: 1 });
19 | expect(getByTestId('min')).not.toHaveAccessibilityValue({ min: 2 });
20 | expect(() => expect(getByTestId('min')).toHaveAccessibilityValue({ min: 2 }))
21 | .toThrowErrorMatchingInlineSnapshot(`
22 | "expect(element).toHaveAccessibilityValue({"min": 2})
23 |
24 | Expected the element to have accessibility value:
25 | {"min": 2}
26 | Received element with accessibility value:
27 | {"min": 1}"
28 | `);
29 |
30 | expect(getByTestId('max')).toHaveAccessibilityValue({ max: 10 });
31 | expect(getByTestId('max')).not.toHaveAccessibilityValue({ max: 5 });
32 | expect(() => expect(getByTestId('max')).toHaveAccessibilityValue({ max: 5 }))
33 | .toThrowErrorMatchingInlineSnapshot(`
34 | "expect(element).toHaveAccessibilityValue({"max": 5})
35 |
36 | Expected the element to have accessibility value:
37 | {"max": 5}
38 | Received element with accessibility value:
39 | {"max": 10}"
40 | `);
41 |
42 | expect(getByTestId('now')).toHaveAccessibilityValue({ now: 5 });
43 | expect(getByTestId('now')).not.toHaveAccessibilityValue({ now: 3 });
44 | expect(() => expect(getByTestId('now')).toHaveAccessibilityValue({ now: 3 }))
45 | .toThrowErrorMatchingInlineSnapshot(`
46 | "expect(element).toHaveAccessibilityValue({"now": 3})
47 |
48 | Expected the element to have accessibility value:
49 | {"now": 3}
50 | Received element with accessibility value:
51 | {"now": 5}"
52 | `);
53 |
54 | expect(getByTestId('min-max')).toHaveAccessibilityValue({ min: 2, max: 5 });
55 | expect(getByTestId('min-max')).not.toHaveAccessibilityValue({ min: 3, max: 5 });
56 | expect(getByTestId('min-max')).not.toHaveAccessibilityValue({ min: 2, max: 4 });
57 | expect(getByTestId('min-max')).not.toHaveAccessibilityValue({ min: 3, max: 4 });
58 |
59 | expect(getByTestId('min-now')).toHaveAccessibilityValue({ min: 2, now: 3 });
60 | expect(getByTestId('min-now')).not.toHaveAccessibilityValue({ min: 1, now: 3 });
61 | expect(getByTestId('min-now')).not.toHaveAccessibilityValue({ min: 2, now: 4 });
62 | expect(getByTestId('min-now')).not.toHaveAccessibilityValue({ min: 0, now: 4 });
63 |
64 | expect(getByTestId('max-now')).toHaveAccessibilityValue({ max: 5, now: 4 });
65 | expect(getByTestId('max-now')).not.toHaveAccessibilityValue({ max: 6, now: 4 });
66 | expect(getByTestId('max-now')).not.toHaveAccessibilityValue({ max: 5, now: 3 });
67 | expect(getByTestId('max-now')).not.toHaveAccessibilityValue({ max: 6, now: 3 });
68 |
69 | expect(getByTestId('min-max-now')).toHaveAccessibilityValue({ min: 2, max: 5, now: 3 });
70 | expect(getByTestId('min-max-now')).not.toHaveAccessibilityValue({ min: 1, max: 5, now: 3 });
71 | expect(getByTestId('min-max-now')).not.toHaveAccessibilityValue({ min: 2, max: 6, now: 3 });
72 | expect(getByTestId('min-max-now')).not.toHaveAccessibilityValue({ min: 2, max: 5, now: 4 });
73 | });
74 |
75 | test('.toHaveAccessibilityValue to handle string text', () => {
76 | const { getByTestId } = render(
77 |
78 |
79 |
80 | ,
81 | );
82 |
83 | expect(getByTestId('text')).toHaveAccessibilityValue({ text: 'Hello world!' });
84 | expect(getByTestId('text')).not.toHaveAccessibilityValue({ text: 'Hello other!' });
85 | expect(() => expect(getByTestId('text')).toHaveAccessibilityValue({ text: 'Hello other!' }))
86 | .toThrowErrorMatchingInlineSnapshot(`
87 | "expect(element).toHaveAccessibilityValue({"text": "Hello other!"})
88 |
89 | Expected the element to have accessibility value:
90 | {"text": "Hello other!"}
91 | Received element with accessibility value:
92 | {"text": "Hello world!"}"
93 | `);
94 | });
95 |
96 | test('.toHaveAccessibilityValue to handle regex text', () => {
97 | const { getByTestId } = render(
98 |
99 |
100 |
101 | ,
102 | );
103 |
104 | expect(getByTestId('text')).toHaveAccessibilityValue({ text: /hello/i });
105 | expect(getByTestId('text')).not.toHaveAccessibilityValue({ text: /other/i });
106 | expect(() => expect(getByTestId('text')).toHaveAccessibilityValue({ text: /other/i }))
107 | .toThrowErrorMatchingInlineSnapshot(`
108 | "expect(element).toHaveAccessibilityValue({"text": /other/i})
109 |
110 | Expected the element to have accessibility value:
111 | {"text": /other/i}
112 | Received element with accessibility value:
113 | {"text": "Hello world!"}"
114 | `);
115 |
116 | expect(getByTestId('text-now')).toHaveAccessibilityValue({ text: /hello/i, now: 5 });
117 | expect(getByTestId('text-now')).not.toHaveAccessibilityValue({ text: /hello/i, now: 3 });
118 | expect(getByTestId('text-now')).not.toHaveAccessibilityValue({ text: /other/i, now: 5 });
119 | expect(() => expect(getByTestId('text-now')).toHaveAccessibilityValue({ text: /hello/i, now: 3 }))
120 | .toThrowErrorMatchingInlineSnapshot(`
121 | "expect(element).toHaveAccessibilityValue({"now": 3, "text": /hello/i})
122 |
123 | Expected the element to have accessibility value:
124 | {"now": 3, "text": /hello/i}
125 | Received element with accessibility value:
126 | {"now": 5, "text": "Hello world!"}"
127 | `);
128 | expect(() => expect(getByTestId('text-now')).toHaveAccessibilityValue({ text: /other/i, now: 5 }))
129 | .toThrowErrorMatchingInlineSnapshot(`
130 | "expect(element).toHaveAccessibilityValue({"now": 5, "text": /other/i})
131 |
132 | Expected the element to have accessibility value:
133 | {"now": 5, "text": /other/i}
134 | Received element with accessibility value:
135 | {"now": 5, "text": "Hello world!"}"
136 | `);
137 | });
138 |
--------------------------------------------------------------------------------
/src/__tests__/to-have-prop.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Text, View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | test('.toHaveProp', () => {
6 | const { queryByTestId } = render(
7 |
8 |
9 | text
10 |
11 |
12 | ,
13 | );
14 |
15 | expect(queryByTestId('button')).toHaveProp('accessibilityState', { disabled: true });
16 | expect(queryByTestId('text')).toHaveProp('ellipsizeMode', 'head');
17 | expect(queryByTestId('text')).toHaveProp('allowFontScaling', false);
18 |
19 | expect(queryByTestId('button')).not.toHaveProp('accessibilityStates');
20 | expect(queryByTestId('button')).not.toHaveProp('ellipsizeMode', undefined);
21 | expect(queryByTestId('button')).not.toHaveProp('allowFontScaling', false);
22 | expect(queryByTestId('text')).not.toHaveProp('style');
23 |
24 | // title is no longer findable as it is a React child
25 | expect(() => expect(queryByTestId('button')).toHaveProp('title', 'ok')).toThrow();
26 | expect(() => expect(queryByTestId('button')).toHaveProp('disabled')).toThrow();
27 | expect(() => expect(queryByTestId('text')).not.toHaveProp('allowFontScaling', false)).toThrow();
28 | expect(() => expect(queryByTestId('text')).toHaveProp('style')).toThrow();
29 | expect(() =>
30 | expect(queryByTestId('text')).toHaveProp('allowFontScaling', 'wrongValue'),
31 | ).toThrow();
32 |
33 | expect(queryByTestId('view')).toHaveProp('style', null);
34 | });
35 |
--------------------------------------------------------------------------------
/src/__tests__/to-have-style.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text, Pressable } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | describe('.toHaveStyle', () => {
6 | test('handles positive test cases', () => {
7 | const styles = StyleSheet.create({ container: { borderBottomColor: 'white' } });
8 | const { getByTestId } = render(
9 |
22 | Hello World
23 | ,
24 | );
25 |
26 | const container = getByTestId('container');
27 |
28 | expect(container).toHaveStyle({ backgroundColor: 'blue', height: '100%' });
29 | expect(container).toHaveStyle([{ backgroundColor: 'blue' }, { height: '100%' }]);
30 | expect(container).toHaveStyle({ backgroundColor: 'blue' });
31 | expect(container).toHaveStyle({ height: '100%' });
32 | expect(container).toHaveStyle({ borderBottomColor: 'white' });
33 | expect(container).toHaveStyle({ width: '50%' });
34 | expect(container).toHaveStyle([[{ width: '50%' }]]);
35 | expect(container).toHaveStyle({ transform: [{ scale: 2 }, { rotate: '45deg' }] });
36 | });
37 |
38 | test('handles negative test cases', () => {
39 | const { getByTestId } = render(
40 |
49 | Hello World
50 | ,
51 | );
52 |
53 | const container = getByTestId('container');
54 | expect(() =>
55 | expect(container).toHaveStyle({ backgroundColor: 'blue', transform: [{ scale: 1 }] }),
56 | ).toThrowErrorMatchingSnapshot();
57 | expect(container).not.toHaveStyle({ fontWeight: 'bold' });
58 | expect(container).not.toHaveStyle({ color: 'black' });
59 | expect(container).not.toHaveStyle({ transform: [{ rotate: '45deg' }, { scale: 2 }] });
60 | expect(container).not.toHaveStyle({ transform: [{ rotate: '45deg' }] });
61 | });
62 |
63 | test('handles when the style prop is undefined', () => {
64 | const { getByTestId } = render(
65 |
66 | Hello World
67 | ,
68 | );
69 |
70 | const container = getByTestId('container');
71 |
72 | expect(container).not.toHaveStyle({ fontWeight: 'bold' });
73 | });
74 |
75 | test('handles transform when transform undefined', () => {
76 | const { getByTestId } = render(
77 |
84 | Hello World
85 | ,
86 | );
87 |
88 | const container = getByTestId('container');
89 | expect(() =>
90 | expect(container).toHaveStyle({ transform: [{ scale: 1 }] }),
91 | ).toThrowErrorMatchingSnapshot();
92 | });
93 |
94 | test('handles Pressable with function style prop', () => {
95 | const { getByTestId } = render(
96 | ({ backgroundColor: 'blue' })} />,
97 | );
98 | expect(getByTestId('test')).toHaveStyle({ backgroundColor: 'blue' });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/__tests__/to-have-text-content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 | import { render } from '@testing-library/react-native';
4 |
5 | describe('.toHaveTextContent', () => {
6 | test('handles positive test cases', () => {
7 | const { queryByTestId } = render(2);
8 |
9 | expect(queryByTestId('count-value')).toHaveTextContent('2');
10 | expect(queryByTestId('count-value')).toHaveTextContent(/2/);
11 | expect(queryByTestId('count-value')).not.toHaveTextContent('21');
12 | });
13 |
14 | test('handles negative test cases', () => {
15 | const { queryByTestId } = render(2);
16 |
17 | expect(() => expect(queryByTestId('count-value2')).toHaveTextContent('2')).toThrow();
18 |
19 | expect(() => expect(queryByTestId('count-value')).toHaveTextContent('3')).toThrow();
20 | expect(() => expect(queryByTestId('count-value')).not.toHaveTextContent('2')).toThrow();
21 | });
22 |
23 | test('normalizes whitespace by default', () => {
24 | const { getByTestId } = render(
25 |
26 | {`
27 | Step
28 | 1
29 | of
30 | 4
31 | `}
32 |
33 | ,
34 | );
35 |
36 | expect(getByTestId('text')).toHaveTextContent('Step 1 of 4');
37 | });
38 |
39 | test('can handle multiple levels', () => {
40 | const { queryByTestId } = render(
41 |
42 | Step 1 of 4
43 | ,
44 | );
45 |
46 | expect(queryByTestId('parent')).toHaveTextContent('Step 1 of 4');
47 | });
48 |
49 | test('can handle multiple levels with content spread across descendants', () => {
50 | const { queryByTestId } = render(
51 |
52 | One
53 | Two
54 |
55 | Three
56 |
57 |
58 | Four
59 | {null}
60 | Five
61 |
62 | Six
63 | Seven
64 |
65 | Eight
66 |
67 | Nine
68 | ,
69 | );
70 |
71 | expect(queryByTestId('parent')).toHaveTextContent('OneTwoThreeFourFiveSixSevenEightNine');
72 | });
73 |
74 | test('can handle multiple levels with no explicit children prop', () => {
75 | const NoChildren = ({ text }: { text: string }) => {text};
76 | const answer = 'Answer';
77 | const { root } = render(
78 |
79 |
80 | {answer}
81 | {': '}
82 |
83 |
84 | {null}
85 | 2
86 | ,
87 | );
88 |
89 | expect(root).toHaveTextContent(/^Answer: 42$/);
90 | });
91 |
92 | test('throws when no match is found', () => {
93 | const { root } = render(Should succeed);
94 |
95 | expect(() => {
96 | expect(root).toHaveTextContent('Should fail');
97 | }).toThrow();
98 | });
99 |
100 | test('does not throw error with empty content', () => {
101 | const { root } = render();
102 | expect(root).toHaveTextContent('');
103 | });
104 |
105 | test('is case-sensitive', () => {
106 | const { root } = render(Sensitive text);
107 |
108 | expect(root).toHaveTextContent('Sensitive text');
109 | expect(root).not.toHaveTextContent('sensitive text');
110 | });
111 |
112 | test('can handle conditional rendering', () => {
113 | const { getByTestId } = render(
114 |
115 | Shown
116 | {false && false}
117 | {null && null}
118 | {undefined && undefined}
119 | ,
120 | );
121 |
122 | expect(getByTestId('parent')).toHaveTextContent(/^Shown$/);
123 | });
124 |
125 | test('can handle text with an interpolated variable', () => {
126 | const variable = 'variable';
127 | const { root } = render(With a {variable});
128 |
129 | expect(root).toHaveTextContent('With a variable');
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/src/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | import { checkReactElement, isEmpty } from '../utils';
2 |
3 | describe('checkReactElement', () => {
4 | test('it does not throw an error for valid native primitives', () => {
5 | expect(() => {
6 | // @ts-expect-error Argument of type '{ type: "text"; }' is not assignable to parameter of type 'ReactTestInstance'. Type '{ type: "text"; }' is missing the following properties from type 'ReactTestInstance': instance, props, parent, children, and 6 more.ts(2345)
7 | checkReactElement({ type: 'Text' }, () => {}, null);
8 | }).not.toThrow();
9 | });
10 |
11 | test('ReactTestInstance does not throw', () => {
12 | expect(() => {
13 | // @ts-expect-error Argument of type '{ _fiber: {}; }' is not assignable to parameter of type 'ReactTestInstance'. Object literal may only specify known properties, and '_fiber' does not exist in type 'ReactTestInstance'.ts(2345)
14 | checkReactElement({ _fiber: {} }, () => {}, null);
15 | }).not.toThrow();
16 | });
17 |
18 | test('it does throw an error for invalid native primitives', () => {
19 | expect(() => {
20 | // @ts-expect-error Argument of type '{ type: "button"; }' is not assignable to parameter of type 'ReactTestInstance'. Type '{ type: "button"; }' is missing the following properties from type 'ReactTestInstance': instance, props, parent, children, and 6 more.ts(2345)
21 | checkReactElement({ type: 'Button' }, () => {}, null);
22 | }).toThrow();
23 | });
24 | });
25 |
26 | test('isEmpty', () => {
27 | expect(isEmpty(null)).toEqual(true);
28 | expect(isEmpty(undefined)).toEqual(true);
29 | expect(isEmpty('')).toEqual(true);
30 | expect(isEmpty(' ')).toEqual(false);
31 | expect(isEmpty([])).toEqual(true);
32 | expect(isEmpty([[]])).toEqual(false);
33 | expect(isEmpty({})).toEqual(true);
34 | expect(isEmpty({ x: 0 })).toEqual(false);
35 | expect(isEmpty(0)).toEqual(true);
36 | expect(isEmpty(1)).toEqual(false);
37 | expect(isEmpty(NaN)).toEqual(true);
38 | expect(isEmpty([''])).toEqual(false);
39 | });
40 |
--------------------------------------------------------------------------------
/src/__types__/jest-explicit-extend.test-d.ts:
--------------------------------------------------------------------------------
1 | // This file checks whether explicit Jest `extend` from '@jest/expect' is correctly extended with Jest Matchers.
2 |
3 | // eslint-disable-next-line import/no-extraneous-dependencies
4 | import { expect as jestExpect } from '@jest/globals';
5 |
6 | jestExpect(null).toBeDisabled();
7 | jestExpect(null).toBeEmptyElement();
8 | jestExpect(null).toBeEnabled();
9 | jestExpect(null).toBeOnTheScreen();
10 | jestExpect(null).toBeVisible();
11 | jestExpect(null).toContainElement(null);
12 | jestExpect(null).toHaveTextContent('');
13 | jestExpect(null).toHaveProp('foo');
14 | jestExpect(null).toHaveStyle({});
15 | jestExpect(null).toHaveAccessibilityState({});
16 | jestExpect(null).toHaveAccessibilityValue({});
17 |
--------------------------------------------------------------------------------
/src/__types__/jest-implicit-extend.test-d.ts:
--------------------------------------------------------------------------------
1 | // This file checks whether implicit Jest `extend` is correctly extended with Jest Matchers.
2 |
3 | expect(null).toBeDisabled();
4 | expect(null).toBeEmptyElement();
5 | expect(null).toBeEnabled();
6 | expect(null).toBeOnTheScreen();
7 | expect(null).toBeVisible();
8 | expect(null).toContainElement(null);
9 | expect(null).toHaveTextContent('');
10 | expect(null).toHaveProp('foo');
11 | expect(null).toHaveStyle({});
12 | expect(null).toHaveAccessibilityState({});
13 | expect(null).toHaveAccessibilityValue({});
14 |
--------------------------------------------------------------------------------
/src/component-tree.ts:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 |
4 | /**
5 | * Checks if the given element is a host element.
6 | * @param element The element to check.
7 | */
8 | export function isHostElement(element?: ReactTestInstance | null): boolean {
9 | return typeof element?.type === 'string';
10 | }
11 |
12 | /**
13 | * Returns first host ancestor for given element or first ancestor of one of
14 | * passed component types.
15 | *
16 | * @param element The element start traversing from.
17 | * @param componentTypes Additional component types to match.
18 | */
19 | export function getParentElement(
20 | element: ReactTestInstance | null,
21 | componentTypes: React.ElementType[] = [],
22 | ): ReactTestInstance | null {
23 | if (element == null) {
24 | return null;
25 | }
26 |
27 | let current = element.parent;
28 | while (current) {
29 | if (isHostElement(current) || componentTypes.includes(current.type)) {
30 | return current;
31 | }
32 |
33 | current = current.parent;
34 | }
35 |
36 | return null;
37 | }
38 |
--------------------------------------------------------------------------------
/src/extend-expect.ts:
--------------------------------------------------------------------------------
1 | import { toBeDisabled, toBeEnabled } from './to-be-disabled';
2 | import { toBeEmptyElement, toBeEmpty } from './to-be-empty-element';
3 | import { toBeOnTheScreen } from './to-be-on-the-screen';
4 | import { toBeVisible } from './to-be-visible';
5 | import { toContainElement } from './to-contain-element';
6 | import { toHaveAccessibilityState } from './to-have-accessibility-state';
7 | import { toHaveAccessibilityValue } from './to-have-accessibility-value';
8 | import { toHaveProp } from './to-have-prop';
9 | import { toHaveStyle } from './to-have-style';
10 | import { toHaveTextContent } from './to-have-text-content';
11 |
12 | expect.extend({
13 | toBeDisabled,
14 | toBeEnabled,
15 | toBeEmptyElement,
16 | toBeEmpty, // Deprecated
17 | toBeOnTheScreen,
18 | toBeVisible,
19 | toContainElement,
20 | toHaveAccessibilityState,
21 | toHaveAccessibilityValue,
22 | toHaveProp,
23 | toHaveStyle,
24 | toHaveTextContent,
25 | });
26 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { toBeDisabled, toBeEnabled } from './to-be-disabled';
2 | export { toBeEmptyElement, toBeEmpty } from './to-be-empty-element';
3 | export { toBeOnTheScreen } from './to-be-on-the-screen';
4 | export { toBeVisible } from './to-be-visible';
5 | export { toContainElement } from './to-contain-element';
6 | export { toHaveAccessibilityState } from './to-have-accessibility-state';
7 | export { toHaveAccessibilityValue } from './to-have-accessibility-value';
8 | export { toHaveProp } from './to-have-prop';
9 | export { toHaveStyle } from './to-have-style';
10 | export { toHaveTextContent } from './to-have-text-content';
11 |
--------------------------------------------------------------------------------
/src/legacy-extend-expect.ts:
--------------------------------------------------------------------------------
1 | import { toBeDisabled, toBeEnabled } from './to-be-disabled';
2 | import { toBeEmptyElement } from './to-be-empty-element';
3 | import { toBeOnTheScreen } from './to-be-on-the-screen';
4 | import { toBeVisible } from './to-be-visible';
5 | import { toContainElement } from './to-contain-element';
6 | import { toHaveAccessibilityState } from './to-have-accessibility-state';
7 | import { toHaveAccessibilityValue } from './to-have-accessibility-value';
8 | import { toHaveProp } from './to-have-prop';
9 | import { toHaveStyle } from './to-have-style';
10 | import { toHaveTextContent } from './to-have-text-content';
11 |
12 | expect.extend({
13 | legacy_toBeDisabled: toBeDisabled,
14 | legacy_toBeEnabled: toBeEnabled,
15 | legacy_toBeEmptyElement: toBeEmptyElement,
16 | legacy_toBeOnTheScreen: toBeOnTheScreen,
17 | legacy_toBeVisible: toBeVisible,
18 | legacy_toContainElement: toContainElement,
19 | legacy_toHaveAccessibilityState: toHaveAccessibilityState,
20 | legacy_toHaveAccessibilityValue: toHaveAccessibilityValue,
21 | legacy_toHaveProp: toHaveProp,
22 | legacy_toHaveStyle: toHaveStyle,
23 | legacy_toHaveTextContent: toHaveTextContent,
24 | });
25 |
--------------------------------------------------------------------------------
/src/to-be-disabled.ts:
--------------------------------------------------------------------------------
1 | import type { ReactTestInstance } from 'react-test-renderer';
2 | import { matcherHint } from 'jest-matcher-utils';
3 | import { checkReactElement, getType, printElement } from './utils';
4 |
5 | // Elements that support 'disabled'
6 | const DISABLE_TYPES = [
7 | 'Button',
8 | 'Slider',
9 | 'Switch',
10 | 'Text',
11 | 'TouchableHighlight',
12 | 'TouchableOpacity',
13 | 'TouchableWithoutFeedback',
14 | 'TouchableNativeFeedback',
15 | 'View',
16 | 'TextInput',
17 | 'Pressable',
18 | ];
19 |
20 | function isElementDisabled(element: ReactTestInstance) {
21 | if (getType(element) === 'TextInput' && element?.props?.editable === false) {
22 | return true;
23 | }
24 |
25 | if (!DISABLE_TYPES.includes(getType(element))) {
26 | return false;
27 | }
28 |
29 | return (
30 | !!element?.props?.disabled ||
31 | !!element?.props?.accessibilityState?.disabled ||
32 | !!element?.props?.accessibilityStates?.includes('disabled')
33 | );
34 | }
35 |
36 | function isAncestorDisabled(element: ReactTestInstance): boolean {
37 | const parent = element.parent;
38 | return parent != null && (isElementDisabled(element) || isAncestorDisabled(parent));
39 | }
40 |
41 | export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstance) {
42 | checkReactElement(element, toBeDisabled, this);
43 |
44 | const isDisabled = isElementDisabled(element) || isAncestorDisabled(element);
45 |
46 | return {
47 | pass: isDisabled,
48 | message: () => {
49 | const is = isDisabled ? 'is' : 'is not';
50 | return [
51 | matcherHint(`${this.isNot ? '.not' : ''}.toBeDisabled`, 'element', ''),
52 | '',
53 | `Received element ${is} disabled:`,
54 | printElement(element),
55 | ].join('\n');
56 | },
57 | };
58 | }
59 |
60 | export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstance) {
61 | checkReactElement(element, toBeEnabled, this);
62 |
63 | const isEnabled = !isElementDisabled(element) && !isAncestorDisabled(element);
64 |
65 | return {
66 | pass: isEnabled,
67 | message: () => {
68 | const is = isEnabled ? 'is' : 'is not';
69 | return [
70 | matcherHint(`${this.isNot ? '.not' : ''}.toBeEnabled`, 'element', ''),
71 | '',
72 | `Received element ${is} enabled:`,
73 | printElement(element),
74 | ].join('\n');
75 | },
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/src/to-be-empty-element.ts:
--------------------------------------------------------------------------------
1 | import type { ReactTestInstance } from 'react-test-renderer';
2 | import { matcherHint } from 'jest-matcher-utils';
3 | import { checkReactElement, isEmpty, printDeprecationWarning, printElement } from './utils';
4 |
5 | export function toBeEmptyElement(this: jest.MatcherContext, element: ReactTestInstance) {
6 | checkReactElement(element, toBeEmptyElement, this);
7 |
8 | return {
9 | pass: isEmpty(element?.props?.children),
10 | message: () => {
11 | return [
12 | matcherHint(`${this.isNot ? '.not' : ''}.toBeEmpty`, 'element', ''),
13 | '',
14 | 'Received:',
15 | printElement(element),
16 | ].join('\n');
17 | },
18 | };
19 | }
20 |
21 | /**
22 | * @deprecated This function has been renamed to `toBeEmptyElement`.
23 | */
24 | export function toBeEmpty(this: jest.MatcherContext, element: ReactTestInstance) {
25 | printDeprecationWarning(
26 | 'toBeEmpty',
27 | `"toBeEmpty()" matcher has been renamed to "toBeEmptyElement()". Old name will be deleted in future versions of @testing-library/jest-native.`,
28 | );
29 | return toBeEmptyElement.call(this, element);
30 | }
31 |
--------------------------------------------------------------------------------
/src/to-be-on-the-screen.ts:
--------------------------------------------------------------------------------
1 | import type { ReactTestInstance } from 'react-test-renderer';
2 | import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3 | import { checkReactElement, printElement } from './utils';
4 |
5 | export function toBeOnTheScreen(this: jest.MatcherContext, element: ReactTestInstance) {
6 | if (element !== null) {
7 | checkReactElement(element, toBeOnTheScreen, this);
8 | }
9 |
10 | const pass = element === null ? false : getScreenRoot() === getRootElement(element);
11 |
12 | const errorFound = () => {
13 | return `expected element tree not to contain element but found:\n${printElement(element)}`;
14 | };
15 |
16 | const errorNotFound = () => {
17 | return `element could not be found in the element tree`;
18 | };
19 |
20 | return {
21 | pass,
22 | message: () => {
23 | return [
24 | matcherHint(`${this.isNot ? '.not' : ''}.toBeOnTheScreen`, 'element', ''),
25 | '',
26 | RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
27 | ].join('\n');
28 | },
29 | };
30 | }
31 |
32 | function getRootElement(element: ReactTestInstance) {
33 | let root = element;
34 | while (root.parent) {
35 | root = root.parent;
36 | }
37 | return root;
38 | }
39 |
40 | function getScreenRoot() {
41 | try {
42 | // eslint-disable-next-line import/no-extraneous-dependencies
43 | const { screen } = require('@testing-library/react-native');
44 | if (!screen) {
45 | throw new Error('screen is undefined');
46 | }
47 |
48 | return screen.UNSAFE_root ?? screen.container;
49 | } catch (error) {
50 | throw new Error(
51 | 'Could not import `screen` object from @testing-library/react-native.\n\n' +
52 | 'Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies.',
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/to-be-visible.ts:
--------------------------------------------------------------------------------
1 | import { Modal, StyleSheet } from 'react-native';
2 | import { matcherHint } from 'jest-matcher-utils';
3 | import type { ReactTestInstance } from 'react-test-renderer';
4 |
5 | import { checkReactElement, printElement } from './utils';
6 | import { getParentElement } from './component-tree';
7 |
8 | function isVisibleForStyles(element: ReactTestInstance) {
9 | const style = element.props.style || {};
10 | const { display, opacity } = StyleSheet.flatten(style);
11 | return display !== 'none' && opacity !== 0;
12 | }
13 |
14 | function isVisibleForAccessibility(element: ReactTestInstance) {
15 | return (
16 | !element.props.accessibilityElementsHidden &&
17 | element.props.importantForAccessibility !== 'no-hide-descendants'
18 | );
19 | }
20 |
21 | function isModalVisible(element: ReactTestInstance) {
22 | return element.type !== Modal || element.props.visible !== false;
23 | }
24 |
25 | function isElementVisible(element: ReactTestInstance): boolean {
26 | let current: ReactTestInstance | null = element;
27 | while (current) {
28 | if (
29 | !isVisibleForStyles(current) ||
30 | !isVisibleForAccessibility(current) ||
31 | !isModalVisible(current)
32 | ) {
33 | return false;
34 | }
35 |
36 | current = getParentElement(current, [Modal]);
37 | }
38 |
39 | return true;
40 | }
41 |
42 | export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) {
43 | checkReactElement(element, toBeVisible, this);
44 | const isVisible = isElementVisible(element);
45 | return {
46 | pass: isVisible,
47 | message: () => {
48 | const is = isVisible ? 'is' : 'is not';
49 | return [
50 | matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
51 | '',
52 | `Received element ${is} visible:`,
53 | printElement(element),
54 | ].join('\n');
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/to-contain-element.ts:
--------------------------------------------------------------------------------
1 | import type { ReactTestInstance } from 'react-test-renderer';
2 | import { matcherHint, RECEIVED_COLOR as receivedColor } from 'jest-matcher-utils';
3 | import { checkReactElement, printElement } from './utils';
4 |
5 | export function toContainElement(
6 | this: jest.MatcherContext,
7 | container: ReactTestInstance,
8 | element: ReactTestInstance | null,
9 | ) {
10 | checkReactElement(container, toContainElement, this);
11 |
12 | if (element !== null) {
13 | checkReactElement(element, toContainElement, this);
14 | }
15 |
16 | let matches = [];
17 |
18 | if (element) {
19 | matches = container.findAll((node) => {
20 | return node.type === element.type && this.equals(node.props, element.props);
21 | });
22 | }
23 |
24 | return {
25 | pass: Boolean(matches.length),
26 | message: () => {
27 | return [
28 | matcherHint(`${this.isNot ? '.not' : ''}.toContainElement`, 'element', 'element'),
29 | '',
30 | receivedColor(`${printElement(container)} ${
31 | this.isNot ? '\n\ncontains:\n\n' : '\n\ndoes not contain:\n\n'
32 | } ${printElement(element)}
33 | `),
34 | ].join('\n');
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/to-have-accessibility-state.ts:
--------------------------------------------------------------------------------
1 | import type { AccessibilityState } from 'react-native';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 | import { matcherHint, stringify } from 'jest-matcher-utils';
4 | import { checkReactElement, getMessage } from './utils';
5 |
6 | export function toHaveAccessibilityState(
7 | this: jest.MatcherContext,
8 | element: ReactTestInstance,
9 | expectedState: AccessibilityState,
10 | ) {
11 | checkReactElement(element, toHaveAccessibilityState, this);
12 |
13 | const impliedState = getAccessibilityState(element);
14 | return {
15 | pass: matchAccessibilityState(element, expectedState),
16 | message: () => {
17 | const matcher = matcherHint(
18 | `${this.isNot ? '.not' : ''}.toHaveAccessibilityState`,
19 | 'element',
20 | stringify(expectedState),
21 | );
22 | return getMessage(
23 | matcher,
24 | `Expected the element ${this.isNot ? 'not to' : 'to'} have accessibility state`,
25 | stringify(expectedState),
26 | 'Received element with implied accessibility state',
27 | stringify(impliedState),
28 | );
29 | },
30 | };
31 | }
32 |
33 | /**
34 | * Default accessibility state values based on experiments using accessibility
35 | * inspector/screen reader on iOS and Android.
36 | *
37 | * @see https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State
38 | */
39 | const defaultState: AccessibilityState = {
40 | disabled: false,
41 | selected: false,
42 | busy: false,
43 | };
44 |
45 | const getAccessibilityState = (element: ReactTestInstance) => {
46 | return {
47 | ...defaultState,
48 | ...element.props.accessibilityState,
49 | };
50 | };
51 |
52 | const accessibilityStateKeys: (keyof AccessibilityState)[] = [
53 | 'disabled',
54 | 'selected',
55 | 'checked',
56 | 'busy',
57 | 'expanded',
58 | ];
59 |
60 | function matchAccessibilityState(element: ReactTestInstance, matcher: AccessibilityState) {
61 | const state = getAccessibilityState(element);
62 | return accessibilityStateKeys.every((key) => matchStateEntry(state, matcher, key));
63 | }
64 |
65 | function matchStateEntry(
66 | state: AccessibilityState,
67 | matcher: AccessibilityState,
68 | key: keyof AccessibilityState,
69 | ) {
70 | return matcher[key] === undefined || matcher[key] === state[key];
71 | }
72 |
--------------------------------------------------------------------------------
/src/to-have-accessibility-value.ts:
--------------------------------------------------------------------------------
1 | import type { AccessibilityValue } from 'react-native';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 | import { matcherHint, stringify } from 'jest-matcher-utils';
4 | import { checkReactElement, getMessage, matches } from './utils';
5 |
6 | export interface AccessibilityValueMatcher {
7 | min?: number;
8 | max?: number;
9 | now?: number;
10 | text?: string | RegExp;
11 | }
12 |
13 | export function toHaveAccessibilityValue(
14 | this: jest.MatcherContext,
15 | element: ReactTestInstance,
16 | expectedValue: AccessibilityValueMatcher,
17 | ) {
18 | checkReactElement(element, toHaveAccessibilityValue, this);
19 |
20 | const value = element.props.accessibilityValue;
21 |
22 | return {
23 | pass: matchAccessibilityValue(value, expectedValue),
24 | message: () => {
25 | const matcher = matcherHint(
26 | `${this.isNot ? '.not' : ''}.toHaveAccessibilityValue`,
27 | 'element',
28 | stringify(expectedValue),
29 | );
30 | return getMessage(
31 | matcher,
32 | `Expected the element ${this.isNot ? 'not to' : 'to'} have accessibility value`,
33 | stringify(expectedValue),
34 | 'Received element with accessibility value',
35 | stringify(value),
36 | );
37 | },
38 | };
39 | }
40 |
41 | function matchAccessibilityValue(
42 | value: AccessibilityValue,
43 | matcher: AccessibilityValueMatcher,
44 | ): boolean {
45 | return (
46 | (matcher.min === undefined || matcher.min === value.min) &&
47 | (matcher.max === undefined || matcher.max === value.max) &&
48 | (matcher.now === undefined || matcher.now === value.now) &&
49 | (matcher.text === undefined || matches(value.text ?? '', matcher.text))
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/to-have-prop.ts:
--------------------------------------------------------------------------------
1 | import type { ReactTestInstance } from 'react-test-renderer';
2 | import { matcherHint, stringify, printExpected } from 'jest-matcher-utils';
3 | import { checkReactElement, getMessage } from './utils';
4 |
5 | function printAttribute(name: string, value: unknown) {
6 | return value === undefined ? name : `${name}=${stringify(value)}`;
7 | }
8 |
9 | function getPropComment(name: string, value: unknown) {
10 | return value === undefined
11 | ? `element.hasProp(${name})`
12 | : `element.getAttribute(${name}) === ${stringify(value)}`;
13 | }
14 |
15 | export function toHaveProp(
16 | this: jest.MatcherContext,
17 | element: ReactTestInstance,
18 | name: string,
19 | expectedValue: unknown,
20 | ) {
21 | checkReactElement(element, toHaveProp, this);
22 |
23 | const prop = element.props[name];
24 |
25 | const isDefined = expectedValue !== undefined;
26 | const hasProp = name in element.props;
27 |
28 | return {
29 | pass: isDefined ? hasProp && this.equals(prop, expectedValue) : hasProp,
30 | message: () => {
31 | const to = this.isNot ? 'not to' : 'to';
32 |
33 | const receivedProp = hasProp ? printAttribute(name, prop) : null;
34 | const matcher = matcherHint(
35 | `${this.isNot ? '.not' : ''}.toHaveProp`,
36 | 'element',
37 | printExpected(name),
38 | {
39 | secondArgument: isDefined ? printExpected(expectedValue) : undefined,
40 | comment: getPropComment(name, expectedValue),
41 | },
42 | );
43 | return getMessage(
44 | matcher,
45 | `Expected the element ${to} have prop`,
46 | printAttribute(name, expectedValue),
47 | 'Received',
48 | receivedProp,
49 | );
50 | },
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/src/to-have-style.ts:
--------------------------------------------------------------------------------
1 | import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
2 | import type { ReactTestInstance } from 'react-test-renderer';
3 | import { StyleSheet } from 'react-native';
4 | import { matcherHint } from 'jest-matcher-utils';
5 | import { diff } from 'jest-diff';
6 | import chalk from 'chalk';
7 | import { checkReactElement } from './utils';
8 |
9 | type Style = TextStyle | ViewStyle | ImageStyle;
10 | type StyleLike = Record;
11 |
12 | function printoutStyles(style: StyleLike) {
13 | return Object.keys(style)
14 | .sort()
15 | .map((prop) =>
16 | Array.isArray(style[prop])
17 | ? `${prop}: ${JSON.stringify(style[prop], null, 2)};`
18 | : `${prop}: ${style[prop]};`,
19 | )
20 | .join('\n');
21 | }
22 |
23 | /**
24 | * Narrows down the properties in received to those with counterparts in expected
25 | */
26 | function narrow(expected: StyleLike, received: StyleLike) {
27 | return Object.keys(received)
28 | .filter((prop) => expected[prop])
29 | .reduce(
30 | (obj, prop) =>
31 | Object.assign(obj, {
32 | [prop]: received[prop],
33 | }),
34 | {},
35 | );
36 | }
37 |
38 | // Highlights only style rules that were expected but were not found in the
39 | // received computed styles
40 | function expectedDiff(expected: StyleLike, received: StyleLike) {
41 | const receivedNarrow = narrow(expected, received);
42 |
43 | const diffOutput = diff(printoutStyles(expected), printoutStyles(receivedNarrow));
44 | // Remove the "+ Received" annotation because this is a one-way diff
45 | return diffOutput?.replace(`${chalk.red('+ Received')}\n`, '') ?? '';
46 | }
47 |
48 | export function toHaveStyle(
49 | this: jest.MatcherContext,
50 | element: ReactTestInstance,
51 | style: StyleProp