├── .all-contributorsrc
├── .eslint-doc-generatorrc.js
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── smoke-test.yml
│ └── validate.yml
├── .gitignore
├── .huskyrc.js
├── .prettierignore
├── .prettierrc.js
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docs
└── rules
│ ├── prefer-checked.md
│ ├── prefer-empty.md
│ ├── prefer-enabled-disabled.md
│ ├── prefer-focus.md
│ ├── prefer-in-document.md
│ ├── prefer-required.md
│ ├── prefer-to-have-attribute.md
│ ├── prefer-to-have-class.md
│ ├── prefer-to-have-style.md
│ ├── prefer-to-have-text-content.md
│ └── prefer-to-have-value.md
├── index.d.ts
├── other
├── CODE_OF_CONDUCT.md
├── MAINTAINING.md
├── USERS.md
└── manual-releases.md
├── package.json
├── smoke-test
└── eslint-remote-tester.config.js
└── src
├── __tests__
├── __fixtures__
│ └── createBannedAttributeTestCases.js
├── index.test.js
├── lib
│ └── rules
│ │ ├── .eslintrc
│ │ ├── no-attribute-checking.js
│ │ ├── prefer-empty.js
│ │ ├── prefer-focus.js
│ │ ├── prefer-in-document.js
│ │ ├── prefer-prefer-to-have-class.js
│ │ ├── prefer-to-have-attribute.js
│ │ ├── prefer-to-have-style.js
│ │ ├── prefer-to-have-text-content.js
│ │ └── prefer-to-have-value.js
├── queries.test.js
└── rule-tester.js
├── assignment-ast.js
├── context.js
├── createBannedAttributeRule.js
├── index.js
├── queries.js
└── rules
├── prefer-checked.js
├── prefer-empty.js
├── prefer-enabled-disabled.js
├── prefer-focus.js
├── prefer-in-document.js
├── prefer-required.js
├── prefer-to-have-attribute.js
├── prefer-to-have-class.js
├── prefer-to-have-style.js
├── prefer-to-have-text-content.js
└── prefer-to-have-value.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "eslint-plugin-jest-dom",
3 | "projectOwner": "testing-library",
4 | "commit": false,
5 | "commitConvention": "none",
6 | "contributorsPerLine": 7,
7 | "imageSize": 100,
8 | "repoHost": "https://github.com",
9 | "repoType": "github",
10 | "skipCi": true,
11 | "files": [
12 | "README.md"
13 | ],
14 | "contributors": [
15 | {
16 | "login": "benmonro",
17 | "name": "Ben Monro",
18 | "avatar_url": "https://avatars3.githubusercontent.com/u/399236?v=4",
19 | "profile": "https://github.com/benmonro",
20 | "contributions": [
21 | "doc",
22 | "code",
23 | "example",
24 | "test"
25 | ]
26 | },
27 | {
28 | "login": "nickmccurdy",
29 | "name": "Nick McCurdy",
30 | "avatar_url": "https://avatars0.githubusercontent.com/u/927220?v=4",
31 | "profile": "https://nickmccurdy.com/",
32 | "contributions": [
33 | "code",
34 | "doc",
35 | "test"
36 | ]
37 | },
38 | {
39 | "login": "gnapse",
40 | "name": "Ernesto García",
41 | "avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4",
42 | "profile": "https://twitter.com/gnapse",
43 | "contributions": [
44 | "doc"
45 | ]
46 | },
47 | {
48 | "login": "zorfling",
49 | "name": "Chris Colborne",
50 | "avatar_url": "https://avatars2.githubusercontent.com/u/101371?v=4",
51 | "profile": "https://chriscolborne.com",
52 | "contributions": [
53 | "code",
54 | "test"
55 | ]
56 | },
57 | {
58 | "login": "MichaelDeBoey",
59 | "name": "Michaël De Boey",
60 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
61 | "profile": "https://michaeldeboey.be",
62 | "contributions": [
63 | "code"
64 | ]
65 | },
66 | {
67 | "login": "ljosberinn",
68 | "name": "Gerrit Alex",
69 | "avatar_url": "https://avatars1.githubusercontent.com/u/29307652?v=4",
70 | "profile": "http://gerritalex.de",
71 | "contributions": [
72 | "code",
73 | "test",
74 | "doc",
75 | "bug"
76 | ]
77 | },
78 | {
79 | "login": "RIP21",
80 | "name": "Andrey Los",
81 | "avatar_url": "https://avatars1.githubusercontent.com/u/3940079?v=4",
82 | "profile": "http://ololos.space/",
83 | "contributions": [
84 | "bug"
85 | ]
86 | },
87 | {
88 | "login": "skovy",
89 | "name": "Spencer Miskoviak",
90 | "avatar_url": "https://avatars1.githubusercontent.com/u/5247455?v=4",
91 | "profile": "https://skovy.dev",
92 | "contributions": [
93 | "code",
94 | "test"
95 | ]
96 | },
97 | {
98 | "login": "atsikov",
99 | "name": "Aleksei Tsikov",
100 | "avatar_url": "https://avatars3.githubusercontent.com/u/1422928?v=4",
101 | "profile": "https://github.com/atsikov",
102 | "contributions": [
103 | "bug"
104 | ]
105 | },
106 | {
107 | "login": "Belco90",
108 | "name": "Mario Beltrán Alarcón",
109 | "avatar_url": "https://avatars1.githubusercontent.com/u/2677072?v=4",
110 | "profile": "https://mario.dev",
111 | "contributions": [
112 | "doc"
113 | ]
114 | },
115 | {
116 | "login": "AriPerkkio",
117 | "name": "Ari Perkkiö",
118 | "avatar_url": "https://avatars2.githubusercontent.com/u/14806298?v=4",
119 | "profile": "https://codepen.io/ariperkkio/",
120 | "contributions": [
121 | "bug",
122 | "code",
123 | "test"
124 | ]
125 | },
126 | {
127 | "login": "AntonNiklasson",
128 | "name": "Anton Niklasson",
129 | "avatar_url": "https://avatars0.githubusercontent.com/u/785676?v=4",
130 | "profile": "http://www.antn.se",
131 | "contributions": [
132 | "code",
133 | "test",
134 | "doc"
135 | ]
136 | },
137 | {
138 | "login": "juzerzarif",
139 | "name": "Juzer Zarif",
140 | "avatar_url": "https://avatars3.githubusercontent.com/u/22772637?v=4",
141 | "profile": "http://juzerzarif.com",
142 | "contributions": [
143 | "code",
144 | "test",
145 | "bug"
146 | ]
147 | },
148 | {
149 | "login": "julienw",
150 | "name": "Julien Wajsberg",
151 | "avatar_url": "https://avatars.githubusercontent.com/u/454175?v=4",
152 | "profile": "http://everlong.org/",
153 | "contributions": [
154 | "code",
155 | "test"
156 | ]
157 | },
158 | {
159 | "login": "G-Rath",
160 | "name": "Gareth Jones",
161 | "avatar_url": "https://avatars.githubusercontent.com/u/3151613?v=4",
162 | "profile": "https://github.com/G-Rath",
163 | "contributions": [
164 | "test",
165 | "code",
166 | "bug"
167 | ]
168 | },
169 | {
170 | "login": "huyenltnguyen",
171 | "name": "Huyen Nguyen",
172 | "avatar_url": "https://avatars.githubusercontent.com/u/25715018?v=4",
173 | "profile": "https://github.com/huyenltnguyen",
174 | "contributions": [
175 | "doc"
176 | ]
177 | },
178 | {
179 | "login": "mdotwills",
180 | "name": "Matthew",
181 | "avatar_url": "https://avatars.githubusercontent.com/u/5505611?v=4",
182 | "profile": "https://github.com/mdotwills",
183 | "contributions": [
184 | "bug",
185 | "code"
186 | ]
187 | }
188 | ]
189 | }
190 |
--------------------------------------------------------------------------------
/.eslint-doc-generatorrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint-doc-generator').GenerateOptions} */
2 | const config = {
3 | ignoreConfig: [
4 | 'all',
5 | 'flat/all',
6 | 'flat/recommended',
7 | ],
8 | };
9 |
10 | module.exports = config;
11 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
13 |
14 | - `eslint-plugin-jest-dom` version:
15 | - `node` version:
16 | - `npm` version:
17 |
18 | Relevant code or config
19 |
20 | ```javascript
21 |
22 | ```
23 |
24 | What you did:
25 |
26 | What happened:
27 |
28 |
29 |
30 | Reproduction repository:
31 |
32 |
36 |
37 | Problem description:
38 |
39 | Suggested solution:
40 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | **What**:
20 |
21 |
22 |
23 | **Why**:
24 |
25 |
26 |
27 | **How**:
28 |
29 |
30 |
31 | **Checklist**:
32 |
33 |
34 |
35 |
36 | - [ ] Documentation
37 | - [ ] Tests
38 | - [ ] Ready to be merged
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/.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/smoke-test.yml:
--------------------------------------------------------------------------------
1 | name: Smoke test
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * SUN"
6 | workflow_dispatch:
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 16
16 | - run: |
17 | npm install
18 | npm run build
19 | npm link
20 | npm link eslint-plugin-jest-dom
21 | - uses: AriPerkkio/eslint-remote-tester-run-action@v4
22 | with:
23 | issue-title: "Results of weekly scheduled smoke test"
24 | eslint-remote-tester-config: smoke-test/eslint-remote-tester.config.js
25 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: validate
2 | on:
3 | push:
4 | branches:
5 | - "+([0-9])?(.{+([0-9]),x}).x"
6 | - "main"
7 | - "next"
8 | - "next-major"
9 | - "beta"
10 | - "alpha"
11 | - "!all-contributors/**"
12 | pull_request:
13 |
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | main:
20 | # ignore all-contributors PRs
21 | if: ${{ !contains(github.head_ref, 'all-contributors') }}
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | eslint: [6, 7, 8, 9]
26 | node: [12.x, 14.x, 16.x, 18.x, 20.x, 21.x]
27 | testing-library-dom: [8, 9, 10]
28 | exclude:
29 | - eslint: 9
30 | node: 12.x
31 | - eslint: 9
32 | node: 14.x
33 | - eslint: 9
34 | node: 16.x
35 | - testing-library-dom: 9
36 | node: 12.x
37 | - testing-library-dom: 10
38 | node: 12.x
39 | - testing-library-dom: 10
40 | node: 14.x
41 | - testing-library-dom: 10
42 | node: 16.x
43 | runs-on: ubuntu-latest
44 | steps:
45 | - name: ⬇️ Checkout repo
46 | uses: actions/checkout@v4
47 |
48 | - name: ⎔ Setup node
49 | uses: actions/setup-node@v4
50 | with:
51 | node-version: ${{ matrix.node }}
52 |
53 | - name: 📥 Download deps
54 | uses: bahmutov/npm-install@v1
55 | with:
56 | useLockFile: false
57 |
58 | # see https://github.com/npm/cli/issues/7349
59 | - if: ${{ matrix.eslint == 9 }}
60 | run: npm un @typescript-eslint/parser
61 |
62 | - name: Install ESLint v${{ matrix.eslint }}
63 | run: npm install --no-save --force eslint@${{ matrix.eslint }}
64 |
65 | - name: Install @testing-library/dom v${{ matrix.testing-library-dom }}
66 | run: npm install --no-save --force @testing-library/dom@${{ matrix.testing-library-dom }}
67 |
68 | - name: Install TypeScript v4
69 | if: ${{ matrix.node == '12.x' }}
70 | run: npm install --no-save --force typescript@4
71 |
72 | - name: ▶️ Run validate script (without linting)
73 | if: ${{ matrix.eslint != 8 }}
74 | run: npm run validate -- build,test:coverage
75 |
76 | - name: ▶️ Run validate script (with linting)
77 | if: ${{ matrix.eslint == 8 }}
78 | run: npm run validate
79 |
80 | - name: ▶️ Ensure docs are up-to-date
81 | if: ${{ matrix.eslint == 8 && matrix.node != '12.x' }}
82 | run: npm run lint:generate-readme-table
83 |
84 | - name: ⬆️ Upload coverage report
85 | uses: codecov/codecov-action@v3
86 |
87 | release:
88 | needs: main
89 | runs-on: ubuntu-latest
90 | if: ${{ github.repository == 'testing-library/eslint-plugin-jest-dom' &&
91 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha',
92 | github.ref) && github.event_name == 'push' }}
93 | steps:
94 | - name: ⬇️ Checkout repo
95 | uses: actions/checkout@v4
96 |
97 | - name: ⎔ Setup node
98 | uses: actions/setup-node@v4
99 | with:
100 | node-version: 16
101 |
102 | - name: 📥 Download deps
103 | uses: bahmutov/npm-install@v1
104 | with:
105 | useLockFile: false
106 |
107 | - name: 🏗 Run build script
108 | run: npm run build
109 |
110 | - run: ls -asl dist
111 |
112 | - name: 🚀 Release
113 | uses: cycjimmy/semantic-release-action@v4
114 | with:
115 | semantic_version: 18
116 | branches: |
117 | [
118 | '+([0-9])?(.{+([0-9]),x}).x',
119 | 'main',
120 | 'next',
121 | 'next-major',
122 | {name: 'beta', prerelease: true},
123 | {name: 'alpha', prerelease: true}
124 | ]
125 | env:
126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
127 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
128 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .DS_Store
5 |
6 | # these cause more harm than good
7 | # when working with contributors
8 | package-lock.json
9 | yarn.lock
10 |
11 | # Smoke test
12 | .cache-eslint-remote-tester
13 | eslint-remote-tester-results
14 |
--------------------------------------------------------------------------------
/.huskyrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require("kcd-scripts/husky");
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .cache-eslint-remote-tester
5 | eslint-remote-tester-results
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at ben.monro+coc@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2019 Ben Monro
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 |
2 |
eslint-plugin-jest-dom
3 |
4 |
ESLint plugin to follow best practices and anticipate common mistakes when writing tests with jest-dom.
5 |
6 |
7 | ---
8 |
9 |
10 | [![Build Status][build-badge]][build]
11 | [![Code Coverage][coverage-badge]][coverage]
12 | [![version][version-badge]][package]
13 | [![downloads][downloads-badge]][npmtrends]
14 | [![MIT License][license-badge]][license]
15 | [![All Contributors][all-contributors-badge]](#contributors-)
16 | [![PRs Welcome][prs-badge]][prs]
17 | [![Code of Conduct][coc-badge]][coc]
18 |
19 |
20 | ## Table of Contents
21 |
22 |
23 |
24 |
25 | - [Installation](#installation)
26 | - [Usage](#usage)
27 | - [Recommended Configuration](#recommended-configuration)
28 | - [Supported Rules](#supported-rules)
29 | - [Issues](#issues)
30 | - [🐛 Bugs](#-bugs)
31 | - [💡 Feature Requests](#-feature-requests)
32 | - [Contributors ✨](#contributors-)
33 | - [LICENSE](#license)
34 |
35 |
36 |
37 | ## Installation
38 |
39 | This module is distributed via [npm][npm] which is bundled with [node][node] and
40 | should be installed as one of your project's `devDependencies`:
41 |
42 | ```
43 | npm install --save-dev eslint-plugin-jest-dom
44 | ```
45 |
46 | This library has a required `peerDependencies` listing for [`ESLint`](https://eslint.org/).
47 |
48 | ## Usage
49 |
50 | > [!NOTE]
51 | >
52 | > `eslint.config.js` is supported, though most of the plugin documentation still
53 | > currently uses `.eslintrc` syntax; compatible versions of configs are available
54 | > prefixed with `flat/` and may be subject to small breaking changes while ESLint
55 | > v9 is being finalized.
56 | >
57 | > Refer to the
58 | > [ESLint documentation on the new configuration file format](https://eslint.org/docs/latest/use/configure/configuration-files-new)
59 | > for more.
60 |
61 | Add `jest-dom` to the plugins section of your `.eslintrc.js` configuration file.
62 | You can omit the `eslint-plugin-` prefix:
63 |
64 | ```javascript
65 | module.exports = {
66 | plugins: ["jest-dom"],
67 | rules: {
68 | // your configuration
69 | },
70 | };
71 | ```
72 |
73 | Then configure the rules you want to use under the rules section.
74 |
75 | ```javascript
76 | module.exports = {
77 | rules: {
78 | "jest-dom/prefer-checked": "error",
79 | "jest-dom/prefer-enabled-disabled": "error",
80 | "jest-dom/prefer-required": "error",
81 | "jest-dom/prefer-to-have-attribute": "error",
82 | },
83 | };
84 | ```
85 |
86 | ## Recommended Configuration
87 |
88 | This plugin exports a recommended configuration that enforces good `jest-dom`
89 | practices _(you can find more info about enabled rules in
90 | [Supported Rules section](#supported-rules))_.
91 |
92 | To enable this configuration with `.eslintrc`, use the `extends` property:
93 |
94 | ```javascript
95 | module.exports = {
96 | extends: "plugin:jest-dom/recommended",
97 | rules: {
98 | // your configuration
99 | },
100 | };
101 | ```
102 |
103 | To enable this configuration with `eslint.config.js`, use
104 | `jestDom.configs['flat/recommended']`:
105 |
106 | ```javascript
107 | module.exports = [
108 | {
109 | files: [
110 | /* glob matching your test files */
111 | ],
112 | ...require("eslint-plugin-jest-dom").configs["flat/recommended"],
113 | },
114 | ];
115 | ```
116 |
117 | ## Supported Rules
118 |
119 |
120 |
121 | 💼 Configurations enabled in.\
122 | ✅ Set in the `recommended` configuration.\
123 | 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
124 | 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
125 |
126 | | Name | Description | 💼 | 🔧 | 💡 |
127 | | :----------------------------------------------------------------------- | :-------------------------------------------------------------------- | :- | :- | :- |
128 | | [prefer-checked](docs/rules/prefer-checked.md) | prefer toBeChecked over checking attributes | ✅ | 🔧 | |
129 | | [prefer-empty](docs/rules/prefer-empty.md) | Prefer toBeEmpty over checking innerHTML | ✅ | 🔧 | |
130 | | [prefer-enabled-disabled](docs/rules/prefer-enabled-disabled.md) | prefer toBeDisabled or toBeEnabled over checking attributes | ✅ | 🔧 | |
131 | | [prefer-focus](docs/rules/prefer-focus.md) | prefer toHaveFocus over checking document.activeElement | ✅ | 🔧 | |
132 | | [prefer-in-document](docs/rules/prefer-in-document.md) | Prefer .toBeInTheDocument() for asserting the existence of a DOM node | ✅ | 🔧 | 💡 |
133 | | [prefer-required](docs/rules/prefer-required.md) | prefer toBeRequired over checking properties | ✅ | 🔧 | |
134 | | [prefer-to-have-attribute](docs/rules/prefer-to-have-attribute.md) | prefer toHaveAttribute over checking getAttribute/hasAttribute | ✅ | 🔧 | |
135 | | [prefer-to-have-class](docs/rules/prefer-to-have-class.md) | prefer toHaveClass over checking element className | ✅ | 🔧 | |
136 | | [prefer-to-have-style](docs/rules/prefer-to-have-style.md) | prefer toHaveStyle over checking element style | ✅ | 🔧 | |
137 | | [prefer-to-have-text-content](docs/rules/prefer-to-have-text-content.md) | Prefer toHaveTextContent over checking element.textContent | ✅ | 🔧 | |
138 | | [prefer-to-have-value](docs/rules/prefer-to-have-value.md) | prefer toHaveValue over checking element.value | ✅ | 🔧 | |
139 |
140 |
141 |
142 | ## Issues
143 |
144 | _Looking to contribute? Look for the [Good First Issue][good-first-issue]
145 | label._
146 |
147 | ### 🐛 Bugs
148 |
149 | Please file an issue for bugs, missing documentation, or unexpected behavior.
150 |
151 | [**See Bugs**][bugs]
152 |
153 | ### 💡 Feature Requests
154 |
155 | Please file an issue to suggest new features. Vote on feature requests by adding
156 | a 👍. This helps maintainers prioritize what to work on.
157 |
158 | [**See Feature Requests**][requests]
159 |
160 | ## Contributors ✨
161 |
162 | Thanks goes to these people ([emoji key][emojis]):
163 |
164 |
165 |
166 |
167 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | This project follows the [all-contributors][all-contributors] specification.
199 | Contributions of any kind welcome!
200 |
201 | ## LICENSE
202 |
203 | MIT
204 |
205 |
206 | [npm]: https://www.npmjs.com
207 | [node]: https://nodejs.org
208 | [build-badge]: https://img.shields.io/github/actions/workflow/status/testing-library/eslint-plugin-jest-dom/validate.yml?logo=github&style=flat-square
209 | [build]: https://github.com/testing-library/eslint-plugin-jest-dom/actions?query=workflow%3Avalidate
210 | [coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/eslint-plugin-jest-dom.svg?style=flat-square
211 | [coverage]: https://codecov.io/github/testing-library/eslint-plugin-jest-dom
212 | [version-badge]: https://img.shields.io/npm/v/eslint-plugin-jest-dom.svg?style=flat-square
213 | [package]: https://www.npmjs.com/package/eslint-plugin-jest-dom
214 | [downloads-badge]: https://img.shields.io/npm/dm/eslint-plugin-jest-dom.svg?style=flat-square
215 | [npmtrends]: http://www.npmtrends.com/eslint-plugin-jest-dom
216 | [license-badge]: https://img.shields.io/npm/l/eslint-plugin-jest-dom.svg?style=flat-square
217 | [license]: https://github.com/testing-library/eslint-plugin-jest-dom/blob/main/LICENSE
218 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
219 | [prs]: http://makeapullrequest.com
220 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
221 | [coc]: https://github.com/testing-library/eslint-plugin-jest-dom/blob/main/other/CODE_OF_CONDUCT.md
222 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key
223 | [all-contributors]: https://github.com/all-contributors/all-contributors
224 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/eslint-plugin-jest-dom?style=flat-square
225 | [bugs]: https://github.com/testing-library/eslint-plugin-jest-dom/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug
226 | [requests]: https://github.com/testing-library/eslint-plugin-jest-dom/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement
227 | [good-first-issue]: https://github.com/testing-library/eslint-plugin-jest-dom/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22
228 |
229 |
--------------------------------------------------------------------------------
/docs/rules/prefer-checked.md:
--------------------------------------------------------------------------------
1 | # Prefer toBeChecked over checking attributes (`jest-dom/prefer-checked`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to prevent false positives and improve readability and should
12 | only be used with the `@testing-library/jest-dom` package. See below for
13 | examples of those potential issues and why this rule is recommended. The rule is
14 | autofixable and will replace any instances of `.toHaveProperty()` or
15 | `.toHaveAttribute()` with `.toBeChecked()` or `not.toBeChecked()` as
16 | appropriate.
17 |
18 | ### False positives
19 |
20 | Consider these 2 snippets:
21 |
22 | ```js
23 | const { getByRole } = render();
24 | const element = getByRole("checkbox");
25 | expect(element).toHaveProperty("checked"); // passes
26 |
27 | const { getByRole } = render();
28 | const element = getByRole("checkbox");
29 | expect(element).toHaveProperty("checked"); // also passes 😱
30 | ```
31 |
32 | ### Readability
33 |
34 | Consider the following snippets:
35 |
36 | ```js
37 | const { getByRole } = render();
38 | const element = getByRole("checkbox");
39 |
40 | expect(element).toHaveAttribute("checked", false); // fails
41 | expect(element).toHaveAttribute("checked", ""); // fails
42 | expect(element).not.toHaveAttribute("checked", ""); // passes
43 |
44 | expect(element).not.toHaveAttribute("checked", true); // passes.
45 | expect(element).not.toHaveAttribute("checked", false); // also passes.
46 | ```
47 |
48 | As you can see, using `toHaveAttribute` in this case is confusing, unintuitive
49 | and can even lead to false positive tests.
50 |
51 | Examples of **incorrect** code for this rule:
52 |
53 | ```js
54 | expect(element).toHaveProperty("checked", true);
55 | expect(element).toHaveAttribute("checked", false);
56 | expect(element).toHaveProperty("checked", something);
57 |
58 | expect(element).toHaveAttribute("checked");
59 | expect(element).not.toHaveProperty("checked");
60 |
61 | expect(element).not.not.toBeChecked();
62 | ```
63 |
64 | Examples of **correct** code for this rule:
65 |
66 | ```js
67 | expect(element).not.toBeChecked();
68 |
69 | expect(element).toBeChecked();
70 |
71 | expect(element).toHaveProperty("value", "foo");
72 |
73 | expect(element).toHaveAttribute("aria-label");
74 | ```
75 |
76 | ## When Not To Use It
77 |
78 | Don't use this rule if you:
79 |
80 | - don't use `jest-dom`
81 | - want to allow `.toHaveProperty('checked', true|false);`
82 |
83 | ## Further reading
84 |
85 | - [toBeChecked](https://github.com/testing-library/jest-dom#toBeChecked)
86 | - [not.toBeChecked](https://github.com/testing-library/jest-dom#not.toBeChecked)
87 |
--------------------------------------------------------------------------------
/docs/rules/prefer-empty.md:
--------------------------------------------------------------------------------
1 | # Prefer toBeEmpty over checking innerHTML (`jest-dom/prefer-empty`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | This rule ensures people will use toBeEmptyDOMElement() rather than checking dom
10 | nodes/properties. It is primarily aimed at consistently using jest-dom for
11 | readability.
12 |
13 | ## Rule Details
14 |
15 | This autofixable rule aims to ensure usage of `.toBeEmptyDOMElement()`
16 |
17 | Examples of **correct** code for this rule:
18 |
19 | ```js
20 | expect(element.innerHTML).toBe("foo");
21 | expect(element.innerHTML).toBe(foo);
22 | expect(element.innerHTML).not.toBe("foo");
23 | expect(element.innerHTML).not.toBe(foo);
24 | expect(element.firstChild).toBe("foo");
25 | expect(element.firstChild).not.toBe("foo");
26 | expect(getByText("foo").innerHTML).toBe("foo");
27 | expect(getByText("foo").innerHTML).not.toBe("foo");
28 | expect(getByText("foo").firstChild).toBe("foo");
29 | expect(getByText("foo").firstChild).not.toBe("foo");
30 | expect(element.innerHTML === "").toBe(true);
31 | expect(element.innerHTML !== "").toBe(true);
32 | expect(element.innerHTML === "").toBe(false);
33 | expect(element.innerHTML !== "").toBe(false);
34 | expect(element.firstChild === null).toBe(true);
35 | expect(element.firstChild !== null).toBe(false);
36 | expect(element.firstChild === null).toBe(false);
37 | ```
38 |
39 | Examples of **incorrect** code for this rule:
40 |
41 | ```js
42 | expect(element.innerHTML).toBe("");
43 | expect(element.innerHTML).toBe(null);
44 | expect(element.innerHTML).not.toBe(null);
45 | expect(element.innerHTML).not.toBe("");
46 | expect(element.firstChild).toBeNull();
47 | expect(element.firstChild).toBe(null);
48 | expect(element.firstChild).not.toBe(null);
49 | expect(element.firstChild).not.toBeNull();
50 | expect(getByText("foo").innerHTML).toBe("");
51 | expect(getByText("foo").innerHTML).toStrictEqual("");
52 | expect(getByText("foo").innerHTML).toStrictEqual(null);
53 | expect(getByText("foo").firstChild).toBe(null);
54 | expect(getByText("foo").firstChild).not.toBe(null);
55 | expect(element.innerHTML === "foo").toBe(true);
56 | ```
57 |
58 | ## When Not To Use It
59 |
60 | Don't use this rule if you don't care if people use `.toBeEmptyDOMElement()`.
61 |
62 | ## Further Reading
63 |
64 |
65 |
66 |
67 |
68 | ## Changelog
69 |
70 | Previously, this rule was using `.toBeEmpty` which has been [deprecated](https://github.com/testing-library/jest-dom/releases/tag/v5.9.0) in `@testing-library/jest-dom@5.9.0`.
71 |
--------------------------------------------------------------------------------
/docs/rules/prefer-enabled-disabled.md:
--------------------------------------------------------------------------------
1 | # Prefer toBeDisabled or toBeEnabled over checking attributes (`jest-dom/prefer-enabled-disabled`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to prevent false positives and improve readability and should
12 | only be used with the `@testing-library/jest-dom` package. See below for
13 | examples of those potential issues and why this rule is recommended. The rule is
14 | autofixable and will replace any instances of `.toHaveProperty()` or
15 | `.toHaveAttribute()` with `.toBeEnabled()` or `toBeDisabled()` as appropriate.
16 |
17 | In addition, to avoid double negatives and confusing syntax,
18 | `expect(element).not.toBeDisabled()` is also reported and auto-fixed to
19 | `expect(element).toBeEnabled()` and vice versa.
20 |
21 | ### False positives
22 |
23 | Consider these 2 snippets:
24 |
25 | ```js
26 | const { getByRole } = render();
27 | const element = getByRole("checkbox");
28 | expect(element).toHaveProperty("disabled"); // passes
29 |
30 | const { getByRole } = render();
31 | const element = getByRole("checkbox");
32 | expect(element).toHaveProperty("disabled"); // also passes 😱
33 | ```
34 |
35 | ### Readability
36 |
37 | Consider the following snippets:
38 |
39 | ```js
40 | const { getByRole } = render();
41 | const element = getByRole("checkbox");
42 |
43 | expect(element).toHaveAttribute("disabled", false); // fails
44 | expect(element).toHaveAttribute("disabled", ""); // fails
45 | expect(element).not.toHaveAttribute("disabled", ""); // passes
46 |
47 | expect(element).not.toHaveAttribute("disabled", true); // passes.
48 | expect(element).not.toHaveAttribute("disabled", false); // also passes.
49 | ```
50 |
51 | As you can see, using `toHaveAttribute` in this case is confusing, unintuitive
52 | and can even lead to false positive tests.
53 |
54 | Examples of **incorrect** code for this rule:
55 |
56 | ```js
57 | expect(element).toHaveProperty("disabled", true);
58 | expect(element).toHaveAttribute("disabled", false);
59 |
60 | expect(element).toHaveAttribute("disabled");
61 | expect(element).not.toHaveProperty("disabled");
62 |
63 | expect(element).not.toBeDisabled();
64 | expect(element).not.toBeEnabled();
65 | ```
66 |
67 | Examples of **correct** code for this rule:
68 |
69 | ```js
70 | expect(element).toBeEnabled();
71 |
72 | expect(element).toBeDisabled();
73 |
74 | expect(element).toHaveProperty("checked", true);
75 |
76 | expect(element).toHaveAttribute("checked");
77 | ```
78 |
79 | ## When Not To Use It
80 |
81 | Don't use this rule if you:
82 |
83 | - don't use `jest-dom`
84 | - want to allow `.toHaveProperty('disabled', true|false);`
85 |
86 | ## Further reading
87 |
88 | - [toBeDisabled](https://github.com/testing-library/jest-dom#tobedisabled)
89 | - [toBeEnabled](https://github.com/testing-library/jest-dom#tobeenabled)
90 |
--------------------------------------------------------------------------------
/docs/rules/prefer-focus.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveFocus over checking document.activeElement (`jest-dom/prefer-focus`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | prefer toHaveFocus over checking document.activeElement (prefer-focus)
10 |
11 | ## Rule Details
12 |
13 | This autofixable rule aims to improve readability & consistency of tests with
14 | the use of the jest-dom matcher `toHaveFocus` rather than checking
15 | document.activeElement.
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```js
20 | expect(document.activeElement).toBe(foo);
21 | expect(window.document.activeElement).toBe(foo);
22 | expect(global.window.document.activeElement).toBe(foo);
23 | expect(global.document.activeElement).toBe(foo);
24 | expect(foo).toBe(global.document.activeElement);
25 | expect(foo).toBe(window.document.activeElement);
26 | expect(foo).toBe(global.window.document.activeElement);
27 | expect(foo).toBe(document.activeElement);
28 | expect(foo).toEqual(document.activeElement);
29 |
30 | expect(document.activeElement).not.toBe(foo);
31 | expect(window.document.activeElement).not.toBe(foo);
32 | expect(global.window.document.activeElement).not.toBe(foo);
33 | expect(global.document.activeElement).not.toBe(foo);
34 | expect(foo).not.toBe(global.document.activeElement);
35 | expect(foo).not.toBe(window.document.activeElement);
36 | expect(foo).not.toBe(global.window.document.activeElement);
37 | expect(foo).not.toBe(document.activeElement);
38 | expect(foo).not.toEqual(document.activeElement);
39 | ```
40 |
41 | Examples of **correct** code for this rule:
42 |
43 | ```js
44 | expect(input).not.toHaveFocus();
45 | expect(document.activeElement).not.toBeNull();
46 |
47 | expect(input).toHaveFocus();
48 | expect(document.activeElement).toBeNull();
49 | ```
50 |
51 | ## When Not To Use It
52 |
53 | If you don't care if people use toHaveFocus
54 |
--------------------------------------------------------------------------------
/docs/rules/prefer-in-document.md:
--------------------------------------------------------------------------------
1 | # Prefer .toBeInTheDocument() for asserting the existence of a DOM node (`jest-dom/prefer-in-document`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule enforces checking existance of DOM nodes using `.toBeInTheDocument()`.
12 | The rule prefers that matcher over various existance checks such as `.toHaveLength(1)`, `.not.toBeNull()` and
13 | similar.
14 | However it's considered OK to use `.toHaveLength(value)` matcher with `*AllBy*` queries.
15 |
16 | Examples of **incorrect** code for this rule:
17 |
18 | ```js
19 | expect(screen.queryByText("foo")).toHaveLength(1);
20 | expect(queryByText("foo")).toHaveLength(1);
21 | expect(wrapper.queryByText("foo")).toHaveLength(1);
22 | expect(queryByText("foo")).toHaveLength(0);
23 | expect(queryByText("foo")).toBeNull();
24 | expect(queryByText("foo")).not.toBeNull();
25 | expect(queryByText("foo")).toBe(null);
26 | expect(queryByText("foo")).not.toBe(null);
27 | expect(queryByText("foo")).toEqual(null);
28 | expect(queryByText("foo")).not.toEqual(null);
29 | expect(queryByText("foo")).toBeDefined();
30 | expect(queryByText("foo")).not.toBeDefined();
31 | expect(queryByText("foo")).toBeTruthy();
32 | expect(queryByText("foo")).not.toBeTruthy();
33 | expect(queryByText("foo")).toBeFalsy();
34 | expect(queryByText("foo")).not.toBeFalsy();
35 |
36 | const foo = screen.getByText("foo");
37 | expect(foo).toHaveLength(1);
38 |
39 | const bar = screen.queryByText("bar");
40 | expect(bar).toHaveLength(0);
41 | ```
42 |
43 | Examples of **correct** code for this rule:
44 |
45 | ```js
46 | expect(screen.getByText("foo").length).toBe(1);
47 | expect(screen.queryByText("foo")).toBeInTheDocument();
48 | expect(await screen.findByText("foo")).toBeInTheDocument();
49 | expect(queryByText("foo")).toBeInTheDocument();
50 | expect(wrapper.queryAllByTestId("foo")).toHaveLength(1);
51 | expect(screen.getAllByLabel("foo-bar")).toHaveLength(2);
52 | expect(notAQuery("foo-bar")).toHaveLength(1);
53 |
54 | const foo = screen.getAllByText("foo");
55 | expect(foo).toHaveLength(3);
56 |
57 | const bar = screen.queryByText("bar");
58 | expect(bar).not.toBeDefined();
59 |
60 | const baz = await screen.findByText("baz");
61 | expect(baz).toBeDefined();
62 | ```
63 |
64 | ## When Not To Use It
65 |
66 | Don't use this rule if you don't care about the added readability and
67 | improvements that `toBeInTheDocument` offers to your expects.
68 |
69 | Note, that `expect(screen.getByText("foo").length).toBe(1)` is valid. If you want to report on that please use [jest/prefer-to-have-length](https://github.com/jest-community/eslint-plugin-jest/blob/HEAD/docs/rules/prefer-to-have-length.md)
70 | ![fixable][] which will first convert those use cases to use `.toHaveLength`.
71 |
72 | ## Further Reading
73 |
74 | - [Docs on toBeInTheDocument](https://github.com/testing-library/jest-dom#tobeinthedocument)
75 |
76 | [fixable]: https://img.shields.io/badge/-fixable-green.svg
77 |
--------------------------------------------------------------------------------
/docs/rules/prefer-required.md:
--------------------------------------------------------------------------------
1 | # Prefer toBeRequired over checking properties (`jest-dom/prefer-required`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | ## Rule Details
10 |
11 | This rule aims to prevent false positives and improve readability and should
12 | only be used with the `@testing-library/jest-dom` package. See below for
13 | examples of those potential issues and why this rule is recommended. The rule is
14 | autofixable and will replace any instances of `.toHaveProperty()` or
15 | `.toHaveAttribute()` with `toBeRequired()` or `not.toBeRequired` as appropriate.
16 |
17 | ### False positives
18 |
19 | Consider these 2 snippets:
20 |
21 | ```js
22 | const { getByRole } = render();
23 | const element = getByRole("checkbox");
24 | expect(element).toHaveProperty("required"); // passes
25 |
26 | const { getByRole } = render();
27 | const element = getByRole("checkbox");
28 | expect(element).toHaveProperty("required"); // also passes 😱
29 | ```
30 |
31 | ### Readability
32 |
33 | Consider the following snippets:
34 |
35 | ```js
36 | const { getByRole } = render();
37 | const element = getByRole("checkbox");
38 |
39 | expect(element).toHaveAttribute("required", false); // fails
40 | expect(element).toHaveAttribute("required", ""); // fails
41 | expect(element).not.toHaveAttribute("required", ""); // passes
42 |
43 | expect(element).not.toHaveAttribute("required", true); // passes.
44 | expect(element).not.toHaveAttribute("required", false); // also passes.
45 | ```
46 |
47 | As you can see, using `toHaveAttribute` in this case is confusing, unintuitive
48 | and can even lead to false positive tests.
49 |
50 | Examples of **incorrect** code for this rule:
51 |
52 | ```js
53 | expect(element).toHaveProperty("required", true);
54 | expect(element).toHaveAttribute("required", false);
55 |
56 | expect(element).toHaveAttribute("required");
57 | expect(element).not.toHaveProperty("required");
58 |
59 | expect(element).not.toBeRequired();
60 | expect(element).not.not.toBeRequired();
61 | ```
62 |
63 | Examples of **correct** code for this rule:
64 |
65 | ```js
66 | expect(element).not.toBeRequired();
67 |
68 | expect(element).toBeRequired();
69 |
70 | expect(element).toHaveProperty("aria-label", "foo");
71 |
72 | expect(element).toHaveAttribute("alt");
73 | ```
74 |
75 | ## When Not To Use It
76 |
77 | Don't use this rule if you:
78 |
79 | - don't use `jest-dom`
80 | - want to allow `.toHaveProperty('required', true|false);`
81 |
82 | ## Further reading
83 |
84 | - [toBeRequired](https://github.com/testing-library/jest-dom#toBeRequired)
85 | - [not.toBeRequired](https://github.com/testing-library/jest-dom#not.toBeRequired)
86 |
--------------------------------------------------------------------------------
/docs/rules/prefer-to-have-attribute.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveAttribute over checking getAttribute/hasAttribute (`jest-dom/prefer-to-have-attribute`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | This rule is an autofixable rule that reports usages of `getAttribute` or
10 | `hasAttribute` in expect statements in preference of using the jest-dom
11 | `toHaveAttribute` matcher.
12 |
13 | ## Rule Details
14 |
15 | This checks the various built in jest-dom matchers when used in conjunction with
16 | get/hasAttribute. The only valid use case if when using greater/less than
17 | matchers since there isn't any equivalent use with `toHaveAttribute()`
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```js
22 | expect(element.getAttribute("foo")).toMatch(/bar/);
23 | expect(element.getAttribute("foo")).toContain("bar");
24 | expect(getByText("thing").getAttribute("foo")).toBe("bar");
25 | expect(getByText("yes").getAttribute("data-blah")).toBe(
26 | expect.stringMatching(/foo/)
27 | );
28 | expect(element.hasAttribute("foo")).toBeTruthy();
29 | ```
30 |
31 | Examples of **correct** code for this rule:
32 |
33 | ```js
34 | expect(element.foo).toBeTruthy();
35 | expect(element.getAttributeNode()).toBeNull();
36 | expect(element.getAttribute("foo")).toBeGreaterThan(2);
37 | expect(element.getAttribute("foo")).toBeLessThan(2);
38 | ```
39 |
40 | ## When Not To Use It
41 |
42 | If you don't care about using built in matchers for checking attributes on dom
43 | elements.
44 |
45 | ## Further Reading
46 |
47 | - [jest-dom toHaveAttribute](https://github.com/testing-library/jest-dom#tohaveattribute)
48 | - [getAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)
49 | - [hasAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
50 |
--------------------------------------------------------------------------------
/docs/rules/prefer-to-have-class.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveClass over checking element className (`jest-dom/prefer-to-have-class`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | This rule is an autofixable rule that reports usages of checking element className or classList in expect statements in preference of using the jest-dom
10 | `toHaveClass` matcher.
11 |
12 | ## Rule Details
13 |
14 | Examples of **incorrect** code for this rule:
15 |
16 | ```js
17 | expect(el.className).toBe("bar");
18 | expect(el.className).not.toBe("bar");
19 | expect(el.className).toHaveProperty("class", "foo");
20 | expect(screen.getByTestId("foo").className).toBe("foo");
21 | expect(el.className).toContain("bar");
22 | expect(el.className).not.toContain("baz");
23 | expect(el).toHaveAttribute("class", "qux");
24 |
25 | expect(el.classList[0]).toBe("foo");
26 | expect(el.classList[0]).toBe("bar");
27 | ```
28 |
29 | Examples of **correct** code for this rule:
30 |
31 | ```js
32 | expect(el).toHaveClass("bar");
33 | expect(el).toHaveStyle({ foo: "bar" });
34 | expect(el.class).toMatchSnapshot();
35 | expect(el.class).toEqual(foo);
36 | ```
37 |
38 | ## When Not To Use It
39 |
40 | If you don't care about using built in matchers for checking class on dom
41 | elements.
42 |
43 | ## Further Reading
44 |
45 | - [jest-dom toHaveStyle](https://github.com/testing-library/jest-dom#tohaveclass)
46 | - [ElementCSSInlineStyle.class](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/class)
47 |
--------------------------------------------------------------------------------
/docs/rules/prefer-to-have-style.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveStyle over checking element style (`jest-dom/prefer-to-have-style`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | This rule is an autofixable rule that reports usages of checking element.style in expect statements in preference of using the jest-dom
10 | `toHaveStyle` matcher.
11 |
12 | ## Rule Details
13 |
14 | Examples of **incorrect** code for this rule:
15 |
16 | ```js
17 | expect(el.style.foo).toBe("bar");
18 | expect(el.style.foo).not.toBe("bar");
19 | expect(el.style).toHaveProperty("background-color", "green");
20 | expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory");
21 | expect(el.style).toContain("background-color");
22 | expect(el.style).not.toContain("background-color");
23 | expect(el).toHaveAttribute(
24 | "style",
25 | "background-color: green; border-width: 10px; color: blue;"
26 | );
27 | ```
28 |
29 | Examples of **correct** code for this rule:
30 |
31 | ```js
32 | expect(el).toHaveStyle({ foo: "bar" });
33 | expect(el.style).toMatchSnapshot();
34 | expect(el.style).toEqual(foo);
35 | ```
36 |
37 | ## When Not To Use It
38 |
39 | If you don't care about using built in matchers for checking style on dom
40 | elements.
41 |
42 | ## Further Reading
43 |
44 | - [jest-dom toHaveStyle](https://github.com/testing-library/jest-dom#tohavestyle)
45 | - [ElementCSSInlineStyle.style](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style)
46 |
--------------------------------------------------------------------------------
/docs/rules/prefer-to-have-text-content.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveTextContent over checking element.textContent (`jest-dom/prefer-to-have-text-content`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | Please describe the origin of the rule here.
10 |
11 | ## Rule Details
12 |
13 | This rule aims to prevent checking of the `textContent` DOM node attribute in
14 | tests and prefer `toHaveTextContent` instead.
15 |
16 | Examples of **incorrect** code for this rule:
17 |
18 | ```js
19 | expect(element.textContent).toBe("foo");
20 | expect(container.firstChild.textContent).toBe("foo");
21 | expect(getByText("foo").textContent).toBe("foo");
22 | expect(screen.getByText("foo").textContent).toBe("foo");
23 | expect(element.textContent).toEqual("foo");
24 | expect(element.textContent).toContain("foo");
25 | expect(element.textContent).not.toBe("foo");
26 | expect(element.textContent).not.toContain("foo");
27 | ```
28 |
29 | Examples of **correct** code for this rule:
30 |
31 | ```js
32 | expect(string).toBe("foo");
33 | expect(element).toHaveTextContent("foo");
34 | expect(container.lastNode).toBe("foo");
35 | ```
36 |
37 | ## When Not To Use It
38 |
39 | Don't use this rule if you don't care about the added readability and
40 | improvements that `toHaveTextContent` offers to your expects.
41 |
42 | ## Further Reading
43 |
44 | - [Docs on toHaveContent](https://github.com/testing-library/jest-dom#tohavetextcontent)
45 |
--------------------------------------------------------------------------------
/docs/rules/prefer-to-have-value.md:
--------------------------------------------------------------------------------
1 | # Prefer toHaveValue over checking element.value (`jest-dom/prefer-to-have-value`)
2 |
3 | 💼 This rule is enabled in the ✅ `recommended` config.
4 |
5 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6 |
7 |
8 |
9 | This rule is an autofixable rule that encourages the use of toHaveValue over checking the value attribute.
10 |
11 | ## Rule Details
12 |
13 | This rule checks for usages of `.value` or `.toHaveAttribute("value")`.
14 | The only valid use case is when using greater/less than
15 | matchers since there isn't any equivalent use with `toHaveValue`
16 |
17 | Examples of **incorrect** code for this rule:
18 |
19 | ```js
20 | expect(element).toHaveAttribute("value", "foo");
21 | expect(element).toHaveProperty("value", "foo");
22 | expect(element.value).toBe("foo");
23 | expect(element.value).not.toEqual("foo");
24 | expect(element.value).not.toStrictEqual("foo");
25 | ```
26 |
27 | Examples of **correct** code for this rule:
28 |
29 | ```js
30 | expect(element).toHaveValue("foo");
31 | expect(element.value).toBeGreaterThan(2);
32 | expect(element.value).toBeLessThan(2);
33 | ```
34 |
35 | ## When Not To Use It
36 |
37 | If you don't care about using built in matchers for checking attributes on dom
38 | elements.
39 |
40 | ## Further Reading
41 |
42 | - [jest-dom toHaveAttribute](https://github.com/testing-library/jest-dom#tohaveattribute)
43 | - [getAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)
44 | - [hasAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
45 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { type Linter, type Rule } from "eslint";
2 |
3 | declare const plugin: {
4 | meta: {
5 | name: string;
6 | version: string;
7 | };
8 | configs: {
9 | all: Linter.LegacyConfig;
10 | recommended: Linter.LegacyConfig;
11 | "flat/all": Linter.FlatConfig;
12 | "flat/recommended": Linter.FlatConfig;
13 | };
14 | rules: {
15 | [key: string]: Rule.RuleModule;
16 | };
17 | };
18 |
19 | export = plugin;
20 |
--------------------------------------------------------------------------------
/other/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of
9 | experience, nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or reject
41 | comments, commits, code, wiki edits, issues, and other contributions that are
42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any
43 | contributor for other behaviors that they deem inappropriate, threatening,
44 | offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at kentcdodds+coc@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an
62 | incident. Further details of specific enforcement policies may be posted
63 | separately.
64 |
65 | Project maintainers who do not follow or enforce the Code of Conduct in good
66 | faith may face temporary or permanent repercussions as determined by other
67 | members of the project's leadership.
68 |
69 | ## Attribution
70 |
71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
73 |
74 | [homepage]: http://contributor-covenant.org
75 | [version]: http://contributor-covenant.org/version/1/4/
76 |
--------------------------------------------------------------------------------
/other/MAINTAINING.md:
--------------------------------------------------------------------------------
1 | # Maintaining
2 |
3 | This is documentation for maintainers of this project.
4 |
5 | ## Code of Conduct
6 |
7 | Please review, understand, and be an example of it. Violations of the code of
8 | conduct are taken seriously, even (especially) for maintainers.
9 |
10 | ## Issues
11 |
12 | We want to support and build the community. We do that best by helping people
13 | learn to solve their own problems. We have an issue template and hopefully most
14 | folks follow it. If it's not clear what the issue is, invite them to create a
15 | minimal reproduction of what they're trying to accomplish or the bug they think
16 | they've found.
17 |
18 | Once it's determined that a code change is necessary, point people to
19 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a
20 | pull request. If they're the one who needs the feature, they're the one who can
21 | build it. If they need some hand holding and you have time to lend a hand,
22 | please do so. It's an investment into another human being, and an investment
23 | into a potential maintainer.
24 |
25 | Remember that this is open source, so the code is not yours, it's ours. If
26 | someone needs a change in the codebase, you don't have to make it happen
27 | yourself. Commit as much time to the project as you want/need to. Nobody can ask
28 | any more of you than that.
29 |
30 | ## Pull Requests
31 |
32 | As a maintainer, you're fine to make your branches on the main repo or on your
33 | own fork. Either way is fine.
34 |
35 | When we receive a pull request, a travis build is kicked off automatically (see
36 | the `.travis.yml` for what runs in the travis build). We avoid merging anything
37 | that breaks the travis build.
38 |
39 | Please review PRs and focus on the code rather than the individual. You never
40 | know when this is someone's first ever PR and we want their experience to be as
41 | positive as possible, so be uplifting and constructive.
42 |
43 | When you merge the pull request, 99% of the time you should use the
44 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/)
45 | feature. This keeps our git history clean, but more importantly, this allows us
46 | to make any necessary changes to the commit message so we release what we want
47 | to release. See the next section on Releases for more about that.
48 |
49 | ## Release
50 |
51 | Our releases are automatic. They happen whenever code lands into `main`. A
52 | travis build gets kicked off and if it's successful, a tool called
53 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is
54 | used to automatically publish a new release to npm as well as a changelog to
55 | GitHub. It is only able to determine the version and whether a release is
56 | necessary by the git commit messages. With this in mind, **please brush up on
57 | [the commit message convention][commit] which drives our releases.**
58 |
59 | > One important note about this: Please make sure that commit messages do NOT
60 | > contain the words "BREAKING CHANGE" in them unless we want to push a major
61 | > version. I've been burned by this more than once where someone will include
62 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not
63 | > a huge deal honestly, but kind of annoying...
64 |
65 | ## Thanks!
66 |
67 | Thank you so much for helping to maintain this project!
68 |
69 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
70 |
--------------------------------------------------------------------------------
/other/USERS.md:
--------------------------------------------------------------------------------
1 | # Users
2 |
3 | If you or your company uses this project, add your name to this list! Eventually
4 | we may have a website to showcase these (wanna build it!?)
5 |
6 | > No users have been added yet!
7 |
8 |
13 |
--------------------------------------------------------------------------------
/other/manual-releases.md:
--------------------------------------------------------------------------------
1 | # manual-releases
2 |
3 | This project has an automated release set up. So things are only released when
4 | there are useful changes in the code that justify a release. But sometimes
5 | things get messed up one way or another and we need to trigger the release
6 | ourselves. When this happens, simply bump the number below and commit that with
7 | the following commit message based on your needs:
8 |
9 | **Major**
10 |
11 | ```
12 | fix(release): manually release a major version
13 |
14 | There was an issue with a major release, so this manual-releases.md
15 | change is to release a new major version.
16 |
17 | Reference: #
18 |
19 | BREAKING CHANGE:
20 | ```
21 |
22 | **Minor**
23 |
24 | ```
25 | feat(release): manually release a minor version
26 |
27 | There was an issue with a minor release, so this manual-releases.md
28 | change is to release a new minor version.
29 |
30 | Reference: #
31 | ```
32 |
33 | **Patch**
34 |
35 | ```
36 | fix(release): manually release a patch version
37 |
38 | There was an issue with a patch release, so this manual-releases.md
39 | change is to release a new patch version.
40 |
41 | Reference: #
42 | ```
43 |
44 | The number of times we've had to do a manual release is: 0
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-jest-dom",
3 | "version": "0.0.0-semantically-released",
4 | "description": "ESLint plugin to follow best practices and anticipate common mistakes when writing tests with jest-dom",
5 | "keywords": [
6 | "eslint",
7 | "eslintplugin",
8 | "eslint-plugin",
9 | "jest-dom",
10 | "testing-library",
11 | "react-testing-library",
12 | "dom-testing-library",
13 | "RTL",
14 | "DTL",
15 | "tests"
16 | ],
17 | "homepage": "https://github.com/testing-library/eslint-plugin-jest-dom#readme",
18 | "bugs": {
19 | "url": "https://github.com/testing-library/eslint-plugin-jest-dom/issues"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/testing-library/eslint-plugin-jest-dom"
24 | },
25 | "license": "MIT",
26 | "author": "Ben Monro",
27 | "main": "dist/index.js",
28 | "types": "index.d.ts",
29 | "files": [
30 | "dist",
31 | "index.d.ts"
32 | ],
33 | "scripts": {
34 | "build": "kcd-scripts build",
35 | "pregenerate-readme-table": "npm run build",
36 | "generate-readme-table": "eslint-doc-generator",
37 | "lint": "kcd-scripts lint",
38 | "lint:generate-readme-table": "npm run generate-readme-table -- --check",
39 | "setup": "npm install && npm run validate -s",
40 | "test": "kcd-scripts test",
41 | "test:coverage": "npm test -- --coverage",
42 | "test:update": "npm test:coverage -- --updateSnapshot",
43 | "validate": "kcd-scripts validate"
44 | },
45 | "eslintConfig": {
46 | "extends": "./node_modules/kcd-scripts/eslint.js",
47 | "rules": {
48 | "consistent-return": "off",
49 | "max-lines-per-function": "off",
50 | "testing-library/no-dom-import": "off"
51 | },
52 | "overrides": [
53 | {
54 | "files": [
55 | "**/*.ts"
56 | ],
57 | "rules": {
58 | "@typescript-eslint/await-thenable": "off",
59 | "@typescript-eslint/dot-notation": "off",
60 | "@typescript-eslint/no-base-to-string": "off",
61 | "@typescript-eslint/no-floating-promises": "off",
62 | "@typescript-eslint/no-implied-eval": "off",
63 | "@typescript-eslint/no-misused-promises": "off",
64 | "@typescript-eslint/no-throw-literal": "off",
65 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
66 | "@typescript-eslint/no-unnecessary-condition": "off",
67 | "@typescript-eslint/no-unnecessary-qualifier": "off",
68 | "@typescript-eslint/no-unnecessary-type-assertion": "off",
69 | "@typescript-eslint/no-unsafe-argument": "off",
70 | "@typescript-eslint/no-unsafe-assignment": "off",
71 | "@typescript-eslint/no-unsafe-call": "off",
72 | "@typescript-eslint/no-unsafe-member-access": "off",
73 | "@typescript-eslint/no-unsafe-return": "off",
74 | "@typescript-eslint/prefer-includes": "off",
75 | "@typescript-eslint/prefer-nullish-coalescing": "off",
76 | "@typescript-eslint/prefer-reduce-type-parameter": "off",
77 | "@typescript-eslint/prefer-string-starts-ends-with": "off",
78 | "@typescript-eslint/restrict-plus-operands": "off",
79 | "@typescript-eslint/return-await": "off",
80 | "@typescript-eslint/switch-exhaustiveness-check": "off",
81 | "@typescript-eslint/unbound-method": "off"
82 | }
83 | }
84 | ]
85 | },
86 | "eslintIgnore": [
87 | "node_modules",
88 | "coverage",
89 | "dist",
90 | "eslint-remote-tester-results"
91 | ],
92 | "dependencies": {
93 | "@babel/runtime": "^7.16.3",
94 | "requireindex": "^1.2.0"
95 | },
96 | "devDependencies": {
97 | "@testing-library/dom": "^8.20.0",
98 | "@typescript-eslint/parser": "^5.9.1",
99 | "eslint": "^8.7.0",
100 | "eslint-doc-generator": "^1.0.0",
101 | "eslint-remote-tester": "^3.0.0",
102 | "eslint-remote-tester-repositories": "^1.0.1",
103 | "kcd-scripts": "^12.0.0",
104 | "semver": "^7.6.0",
105 | "typescript": "^5.1.3"
106 | },
107 | "peerDependencies": {
108 | "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0",
109 | "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
110 | },
111 | "peerDependenciesMeta": {
112 | "@testing-library/dom": {
113 | "optional": true
114 | }
115 | },
116 | "engines": {
117 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0",
118 | "npm": ">=6",
119 | "yarn": ">=1"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/smoke-test/eslint-remote-tester.config.js:
--------------------------------------------------------------------------------
1 | const {
2 | getRepositories,
3 | getPathIgnorePattern,
4 | } = require("eslint-remote-tester-repositories");
5 |
6 | module.exports = {
7 | repositories: getRepositories({ randomize: true }),
8 | pathIgnorePattern: getPathIgnorePattern(),
9 | extensions: ["js", "jsx", "ts", "tsx"],
10 | concurrentTasks: 3,
11 | cache: false,
12 | logLevel: "info",
13 | eslintrc: {
14 | root: true,
15 | env: {
16 | es6: true,
17 | },
18 | parser: "@typescript-eslint/parser",
19 | parserOptions: {
20 | ecmaVersion: 2020,
21 | sourceType: "module",
22 | ecmaFeatures: {
23 | jsx: true,
24 | },
25 | },
26 | extends: ["plugin:jest-dom/all"],
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/createBannedAttributeTestCases.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-lines-per-function */
2 |
3 | export default ({ preferred, negatedPreferred, attribute }) => {
4 | const doubleNegativeCases = negatedPreferred.startsWith("toBe")
5 | ? [
6 | {
7 | code: `expect().not.${negatedPreferred}`,
8 | errors: [
9 | {
10 | message: `Use ${preferred} instead of not.${negatedPreferred}`,
11 | },
12 | ],
13 | output: `expect().${preferred}`,
14 | },
15 | {
16 | code: `const el = screen.getByText("foo"); expect(el).not.${negatedPreferred}`,
17 | errors: [
18 | {
19 | message: `Use ${preferred} instead of not.${negatedPreferred}`,
20 | },
21 | ],
22 | output: `const el = screen.getByText("foo"); expect(el).${preferred}`,
23 | },
24 | {
25 | code: `expect(getByText("foo")).not.${negatedPreferred}`,
26 | errors: [
27 | {
28 | message: `Use ${preferred} instead of not.${negatedPreferred}`,
29 | },
30 | ],
31 | output: `expect(getByText("foo")).${preferred}`,
32 | },
33 | {
34 | code: `const el = screen.getByText("foo"); expect(el).not.${preferred}`,
35 | errors: [
36 | {
37 | message: `Use ${negatedPreferred} instead of not.${preferred}`,
38 | },
39 | ],
40 | output: `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
41 | },
42 | {
43 | code: `const el = screen.getByRole("button"); expect(el).not.${preferred}`,
44 | errors: [
45 | {
46 | message: `Use ${negatedPreferred} instead of not.${preferred}`,
47 | },
48 | ],
49 | output: `const el = screen.getByRole("button"); expect(el).${negatedPreferred}`,
50 | },
51 | ]
52 | : [];
53 | const directChecks = /-/.test(attribute)
54 | ? []
55 | : [
56 | {
57 | code: `expect(getByText('foo').${attribute}).toBeTruthy()`,
58 | errors: [
59 | {
60 | message: `Use ${preferred} instead of checking .${attribute} directly`,
61 | },
62 | ],
63 | output: `expect(getByText('foo')).${preferred}`,
64 | },
65 | {
66 | code: `expect(getByText('foo').${attribute}).toBeFalsy()`,
67 | errors: [
68 | {
69 | message: `Use ${negatedPreferred} instead of checking .${attribute} directly`,
70 | },
71 | ],
72 | output: `expect(getByText('foo')).${negatedPreferred}`,
73 | },
74 | {
75 | code: `const el = getByText('foo'); expect(el.${attribute}).toBe(true)`,
76 | errors: [
77 | {
78 | message: `Use ${preferred} instead of checking .${attribute} directly`,
79 | },
80 | ],
81 | output: `const el = getByText('foo'); expect(el).${preferred}`,
82 | },
83 | {
84 | code: `const el = getByRole('button'); expect(el.${attribute}).toBe(true)`,
85 | errors: [
86 | {
87 | message: `Use ${preferred} instead of checking .${attribute} directly`,
88 | },
89 | ],
90 | output: `const el = getByRole('button'); expect(el).${preferred}`,
91 | },
92 | ];
93 |
94 | return {
95 | valid: [
96 | `expect().not.toHaveProperty('value', 'foo')`,
97 | `const el = screen.getByText("foo"); expect(el).not.toHaveProperty('value', 'foo')`,
98 | `const el = screen.getByText("foo"); expect(el).${preferred}`,
99 | `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
100 | `const el = screen.getByText("foo"); expect(el).toHaveProperty('value', 'bar')`,
101 | `const el = foo.bar(); expect(el).toHaveProperty("${attribute}", true)`,
102 | `expect(getFoo().${attribute}).toBe("bar")`,
103 | `expect(getFoo().${attribute}).not.toBe("bar")`,
104 | ],
105 | invalid: [
106 | ...doubleNegativeCases,
107 | ...directChecks,
108 | {
109 | code: `const el = screen.getByText("foo"); expect(el).toHaveProperty('${attribute}', true)`,
110 | errors: [
111 | {
112 | message: `Use ${preferred} instead of toHaveProperty('${attribute}', true)`,
113 | },
114 | ],
115 | output: `const el = screen.getByText("foo"); expect(el).${preferred}`,
116 | },
117 | {
118 | code: `const el = screen.getByText("foo"); expect(el).toHaveProperty('${attribute}', false)`,
119 | errors: [
120 | {
121 | message: `Use ${negatedPreferred} instead of toHaveProperty('${attribute}', false)`,
122 | },
123 | ],
124 | output: `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
125 | },
126 | {
127 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute('${attribute}', false)`,
128 | errors: [
129 | {
130 | message: `Use ${negatedPreferred} instead of toHaveAttribute('${attribute}', false)`,
131 | },
132 | ],
133 | output: `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
134 | },
135 | {
136 | code: `const el = screen.getByText("foo"); expect(el).toHaveProperty('${attribute}')`,
137 | errors: [
138 | {
139 | message: `Use ${preferred} instead of toHaveProperty('${attribute}')`,
140 | },
141 | ],
142 | output: `const el = screen.getByText("foo"); expect(el).${preferred}`,
143 | },
144 | {
145 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute('${attribute}')`,
146 | errors: [
147 | {
148 | message: `Use ${preferred} instead of toHaveAttribute('${attribute}')`,
149 | },
150 | ],
151 | output: `const el = screen.getByText("foo"); expect(el).${preferred}`,
152 | },
153 | {
154 | code: `const el = screen.getByText("foo"); expect(el).not.toHaveAttribute('${attribute}')`,
155 | errors: [
156 | {
157 | message: `Use ${negatedPreferred} instead of not.toHaveAttribute('${attribute}')`,
158 | },
159 | ],
160 | output: `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
161 | },
162 | {
163 | code: `const el = screen.getByText("foo"); expect(el).not.toHaveProperty('${attribute}')`,
164 | errors: [
165 | {
166 | message: `Use ${negatedPreferred} instead of not.toHaveProperty('${attribute}')`,
167 | },
168 | ],
169 | output: `const el = screen.getByText("foo"); expect(el).${negatedPreferred}`,
170 | },
171 | {
172 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute("${attribute}", "")`,
173 | errors: [
174 | {
175 | message: `Use ${preferred} instead of toHaveAttribute("${attribute}", "")`,
176 | },
177 | ],
178 | output: `const el = screen.getByText("foo"); expect(el).${preferred}`,
179 | },
180 | {
181 | code: `expect(getByText("foo")).toHaveAttribute("${attribute}", "true")`,
182 | errors: [
183 | {
184 | message: `Use ${preferred} instead of toHaveAttribute("${attribute}", "true")`,
185 | },
186 | ],
187 | output: `expect(getByText("foo")).${preferred}`,
188 | },
189 | {
190 | code: `expect(getByText("foo")).toHaveAttribute("${attribute}", "false")`,
191 | errors: [
192 | {
193 | message: `Use ${negatedPreferred} instead of toHaveAttribute("${attribute}", "false")`,
194 | },
195 | ],
196 | output: `expect(getByText("foo")).${negatedPreferred}`,
197 | },
198 | {
199 | code: `expect(getByText("foo")).toHaveAttribute("${attribute}", "")`,
200 | errors: [
201 | {
202 | message: `Use ${preferred} instead of toHaveAttribute("${attribute}", "")`,
203 | },
204 | ],
205 | output: `expect(getByText("foo")).${preferred}`,
206 | },
207 | {
208 | code: `expect(getByText("foo")).not.toHaveProperty("${attribute}")`,
209 | errors: [
210 | {
211 | message: `Use ${negatedPreferred} instead of not.toHaveProperty('${attribute}')`,
212 | },
213 | ],
214 | output: `expect(getByText("foo")).${negatedPreferred}`,
215 | },
216 | {
217 | code: `const el = screen.getByText("foo"); expect(el).toHaveProperty('${attribute}', foo)`,
218 | errors: [
219 | {
220 | message: `Use ${preferred} instead of toHaveProperty('${attribute}', foo)`,
221 | },
222 | ],
223 | },
224 | {
225 | code: `const el = getByRole("button", { name: 'My Button' }); expect(el).toHaveProperty('${attribute}', foo)`,
226 | errors: [
227 | {
228 | message: `Use ${preferred} instead of toHaveProperty('${attribute}', foo)`,
229 | },
230 | ],
231 | },
232 | ],
233 | };
234 | };
235 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | import plugin, { configs, rules } from "../";
2 |
3 | it("includes the configs and rules on the plugin", () => {
4 | expect(plugin).toHaveProperty("configs", configs);
5 | expect(plugin).toHaveProperty("rules", rules);
6 | });
7 |
8 | it("should have all the rules", () => {
9 | expect(Object.keys(rules)).toHaveLength(11);
10 | });
11 |
12 | it.each(Object.entries(rules))(
13 | "%s should export required fields",
14 | (name, rule) => {
15 | expect(rule).toHaveProperty("create", expect.any(Function));
16 | expect(rule.meta.docs.url).not.toBe("");
17 | expect(rule.meta.docs.category).toBe("Best Practices");
18 | expect(rule.meta.docs.description).not.toBe("");
19 | }
20 | );
21 |
22 | it("has the expected recommended config", () => {
23 | expect(configs.recommended).toMatchInlineSnapshot(`
24 | Object {
25 | plugins: Array [
26 | jest-dom,
27 | ],
28 | rules: Object {
29 | jest-dom/prefer-checked: error,
30 | jest-dom/prefer-empty: error,
31 | jest-dom/prefer-enabled-disabled: error,
32 | jest-dom/prefer-focus: error,
33 | jest-dom/prefer-in-document: error,
34 | jest-dom/prefer-required: error,
35 | jest-dom/prefer-to-have-attribute: error,
36 | jest-dom/prefer-to-have-class: error,
37 | jest-dom/prefer-to-have-style: error,
38 | jest-dom/prefer-to-have-text-content: error,
39 | jest-dom/prefer-to-have-value: error,
40 | },
41 | }
42 | `);
43 | });
44 |
45 | it("has the expected recommended flat config", () => {
46 | const expectJestDomPlugin = expect.objectContaining({
47 | meta: {
48 | name: "eslint-plugin-jest-dom",
49 | version: expect.any(String),
50 | },
51 | });
52 |
53 | expect(configs["flat/recommended"]).toMatchInlineSnapshot(
54 | { plugins: { "jest-dom": expectJestDomPlugin } },
55 | `
56 | Object {
57 | plugins: Object {
58 | jest-dom: ObjectContaining {
59 | meta: Object {
60 | name: eslint-plugin-jest-dom,
61 | version: Any,
62 | },
63 | },
64 | },
65 | rules: Object {
66 | jest-dom/prefer-checked: error,
67 | jest-dom/prefer-empty: error,
68 | jest-dom/prefer-enabled-disabled: error,
69 | jest-dom/prefer-focus: error,
70 | jest-dom/prefer-in-document: error,
71 | jest-dom/prefer-required: error,
72 | jest-dom/prefer-to-have-attribute: error,
73 | jest-dom/prefer-to-have-class: error,
74 | jest-dom/prefer-to-have-style: error,
75 | jest-dom/prefer-to-have-text-content: error,
76 | jest-dom/prefer-to-have-value: error,
77 | },
78 | }
79 | `
80 | );
81 | });
82 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/.eslintrc:
--------------------------------------------------------------------------------
1 | {"rules":{
2 | "no-template-curly-in-string":"off"
3 | }}
4 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/no-attribute-checking.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
3 | * @author Ben Monro
4 | */
5 |
6 | import { RuleTester } from "eslint";
7 | import createBannedAttributeTestCases from "../../__fixtures__/createBannedAttributeTestCases";
8 |
9 | const bannedAttributes = [
10 | {
11 | preferred: "toBeDisabled()",
12 | negatedPreferred: "toBeEnabled()",
13 | attributes: ["disabled"],
14 | ruleName: "prefer-enabled-disabled",
15 | },
16 | {
17 | preferred: "toBeRequired()",
18 | negatedPreferred: "not.toBeRequired()",
19 | attributes: ["required", "aria-required"],
20 | ruleName: "prefer-required",
21 | },
22 | {
23 | preferred: "toBeChecked()",
24 | negatedPreferred: "not.toBeChecked()",
25 | attributes: ["checked", "aria-checked"],
26 | ruleName: "prefer-checked",
27 | },
28 | ];
29 |
30 | bannedAttributes.forEach(
31 | ({ preferred, negatedPreferred, attributes, ruleName }) => {
32 | const rule = require(`../../../rules/${ruleName}`);
33 |
34 | // const preferred = 'toBeDisabled()';
35 | // const negatedPreferred = 'toBeEnabled()';
36 | // const attributes = ['disabled'];
37 | const ruleTester = new RuleTester({
38 | parserOptions: { ecmaVersion: 2015, sourceType: "module" },
39 | });
40 | attributes.forEach((attribute) => {
41 | ruleTester.run(
42 | ruleName,
43 | rule,
44 | createBannedAttributeTestCases({
45 | preferred,
46 | negatedPreferred,
47 | attribute,
48 | })
49 | );
50 | });
51 | }
52 | );
53 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-empty.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 | /**
3 | * @fileoverview Prefer toBeEmptyDOMElement over checking innerHTML
4 | * @author Ben Monro
5 | */
6 |
7 | //------------------------------------------------------------------------------
8 | // Requirements
9 | //------------------------------------------------------------------------------
10 |
11 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
12 | import * as rule from '../../../rules/prefer-empty';
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
19 | ruleTester.run("prefer-empty", rule, {
20 | valid: [
21 | `expect().toBe(true)`,
22 | `expect(element.innerHTML).toBe('foo')`,
23 | `expect(element.innerHTML).toBe(foo)`,
24 | `expect(element.innerHTML).toBe(foo + bar)`,
25 | `expect(element.innerHTML).toBe(foo())`,
26 | `expect(element.innerHTML).toBe(foo().bar)`,
27 | `expect(element.innerHTML).toBe(foo.bar)`,
28 | `expect(element.innerHTML).not.toBe(foo + bar)`,
29 | `expect(element.innerHTML).not.toBe(foo())`,
30 | `expect(element.innerHTML).not.toBe(foo().bar)`,
31 | `expect(element.innerHTML).not.toBe(foo.bar)`,
32 | `expect(element.innerHTML).not.toBe('foo')`,
33 | `expect(element.innerHTML).not.toBe(foo)`,
34 | "expect(statusText.innerHTML).toBe(`${value}%`)",
35 | "expect(statusText.innerHTML).not.toBe(`${value}%`)",
36 | "expect(statusText.innerHTML).toBe(`value`)",
37 | "expect(statusText.innerHTML).not.toBe(`value`)",
38 | "expect(statusText.innerHTML).toBe(` `)",
39 | "expect(statusText.innerHTML).not.toBe(` `)",
40 | `expect(element.firstChild).toBe('foo')`,
41 | `expect(element.firstChild).not.toBe('foo')`,
42 | "expect(element.firstChild).toBe(`foo`)",
43 | 'expect(screen.getByText("foo").innerHTML).toBe(`foo ${bar}`)',
44 | `expect(getByText("foo").innerHTML).toBe('foo')`,
45 | `expect(getByText("foo").innerHTML).not.toBe('foo')`,
46 | `expect(getByText("foo").firstChild).toBe('foo')`,
47 | `expect(getByText("foo").firstChild).not.toBe('foo')`,
48 | `expect(element.innerHTML === 'foo').toBe(true)`,
49 | `expect(element.innerHTML !== 'foo').toBe(true)`,
50 | ],
51 |
52 | invalid: [
53 | {
54 | code: `expect(element.innerHTML === '').toBe(true)`,
55 | errors: [
56 | {
57 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
58 | },
59 | ],
60 | output: `expect(element).toBeEmptyDOMElement()`,
61 | },
62 | {
63 | code: `expect(element.innerHTML !== '').toBe(true)`,
64 | errors: [
65 | {
66 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
67 | },
68 | ],
69 | output: `expect(element).not.toBeEmptyDOMElement()`,
70 | },
71 | {
72 | code: `expect(element.innerHTML === '').toBe(false)`,
73 | errors: [
74 | {
75 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
76 | },
77 | ],
78 | output: `expect(element).not.toBeEmptyDOMElement()`,
79 | },
80 | {
81 | code: `expect(element.innerHTML !== '').toBe(false)`,
82 | errors: [
83 | {
84 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
85 | },
86 | ],
87 | output: `expect(element).toBeEmptyDOMElement()`,
88 | },
89 | {
90 | code: `expect(element.firstChild === null).toBe(true)`,
91 | errors: [
92 | {
93 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
94 | },
95 | ],
96 | output: `expect(element).toBeEmptyDOMElement()`,
97 | },
98 | {
99 | code: `expect(element.firstChild !== null).toBe(false)`,
100 | errors: [
101 | {
102 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
103 | },
104 | ],
105 | output: `expect(element).toBeEmptyDOMElement()`,
106 | },
107 | {
108 | code: `expect(element.firstChild === null).toBe(false)`,
109 | errors: [
110 | {
111 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
112 | },
113 | ],
114 | output: `expect(element).not.toBeEmptyDOMElement()`,
115 | },
116 | {
117 | code: `expect(element.innerHTML).toBe('')`,
118 | errors: [
119 | {
120 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
121 | },
122 | ],
123 | output: `expect(element).toBeEmptyDOMElement()`,
124 | },
125 | {
126 | code: "expect(element.innerHTML).toBe(``)",
127 | errors: [
128 | {
129 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
130 | },
131 | ],
132 | output: `expect(element).toBeEmptyDOMElement()`,
133 | },
134 |
135 | {
136 | code: `expect(element.innerHTML).toBe(null)`,
137 | errors: [
138 | {
139 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
140 | },
141 | ],
142 | output: `expect(element).toBeEmptyDOMElement()`,
143 | },
144 | {
145 | code: `expect(element.innerHTML).not.toBe(null)`,
146 | errors: [
147 | {
148 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
149 | },
150 | ],
151 | output: `expect(element).not.toBeEmptyDOMElement()`,
152 | },
153 |
154 | {
155 | code: `expect(element.innerHTML).not.toBe('')`,
156 | errors: [
157 | {
158 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
159 | },
160 | ],
161 | output: `expect(element).not.toBeEmptyDOMElement()`,
162 | },
163 | {
164 | code: "expect(element.innerHTML).not.toBe(``)",
165 | errors: [
166 | {
167 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
168 | },
169 | ],
170 | output: `expect(element).not.toBeEmptyDOMElement()`,
171 | },
172 |
173 | {
174 | code: `expect(element.firstChild).toBeNull()`,
175 | errors: [
176 | {
177 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
178 | },
179 | ],
180 | output: `expect(element).toBeEmptyDOMElement()`,
181 | },
182 | {
183 | code: `expect(element.firstChild).toBe(null)`,
184 | errors: [
185 | {
186 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
187 | },
188 | ],
189 | output: `expect(element).toBeEmptyDOMElement()`,
190 | },
191 | {
192 | code: `expect(element.firstChild).not.toBe(null)`,
193 | errors: [
194 | {
195 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
196 | },
197 | ],
198 | output: `expect(element).not.toBeEmptyDOMElement()`,
199 | },
200 |
201 | {
202 | code: `expect(element.firstChild).not.toBeNull()`,
203 | errors: [
204 | {
205 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
206 | },
207 | ],
208 | output: `expect(element).not.toBeEmptyDOMElement()`,
209 | },
210 | {
211 | code: `expect(getByText('foo').innerHTML).toBe('')`,
212 | errors: [
213 | {
214 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
215 | },
216 | ],
217 | output: `expect(getByText('foo')).toBeEmptyDOMElement()`,
218 | },
219 |
220 | {
221 | code: `expect(getByText('foo').innerHTML).toStrictEqual('')`,
222 | errors: [
223 | {
224 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
225 | },
226 | ],
227 | output: `expect(getByText('foo')).toBeEmptyDOMElement()`,
228 | },
229 |
230 | {
231 | code: `expect(getByText('foo').innerHTML).toStrictEqual(null)`,
232 | errors: [
233 | {
234 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
235 | },
236 | ],
237 | output: `expect(getByText('foo')).toBeEmptyDOMElement()`,
238 | },
239 |
240 | {
241 | code: `expect(getByText('foo').firstChild).toBe(null)`,
242 | errors: [
243 | {
244 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
245 | },
246 | ],
247 | output: `expect(getByText('foo')).toBeEmptyDOMElement()`,
248 | },
249 | {
250 | code: `expect(getByText('foo').firstChild).not.toBe(null)`,
251 | errors: [
252 | {
253 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
254 | },
255 | ],
256 | output: `expect(getByText('foo')).not.toBeEmptyDOMElement()`,
257 | },
258 | ],
259 | });
260 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-focus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveFocus over checking document.activeElement
3 | * @author Ben Monro
4 | */
5 |
6 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
7 | import * as rule from "../../../rules/prefer-focus";
8 |
9 | const ruleTester = new RuleTester();
10 | ruleTester.run("prefer-focus", rule, {
11 | valid: [
12 | `expect().toBe(true)`,
13 | `expect(input).not.toHaveFocus();`,
14 | `expect(input).toHaveFocus();`,
15 | `expect(document.activeElement).toBeNull()`,
16 | `expect(document.activeElement).not.toBeNull()`,
17 | ],
18 |
19 | invalid: [
20 | {
21 | code: "expect(document.activeElement).toBe(foo)",
22 | errors: [
23 | {
24 | message: "Use toHaveFocus instead of checking activeElement",
25 | },
26 | ],
27 | output: "expect(foo).toHaveFocus()",
28 | },
29 | {
30 | code: `expect(document.activeElement).toBe(getByText('Foo'))`,
31 | errors: [
32 | {
33 | message: "Use toHaveFocus instead of checking activeElement",
34 | },
35 | ],
36 | output: `expect(getByText('Foo')).toHaveFocus()`,
37 | },
38 | {
39 | code: `expect(document.activeElement).not.toBe(getByText('Foo'))`,
40 | errors: [
41 | {
42 | message: "Use toHaveFocus instead of checking activeElement",
43 | },
44 | ],
45 | output: `expect(getByText('Foo')).not.toHaveFocus()`,
46 | },
47 | {
48 | code: "expect(document.activeElement).not.toBe(foo)",
49 | errors: [
50 | {
51 | message: "Use toHaveFocus instead of checking activeElement",
52 | },
53 | ],
54 | output: "expect(foo).not.toHaveFocus()",
55 | },
56 | {
57 | code: "expect(foo).not.toBe(document.activeElement)",
58 | errors: [
59 | {
60 | message: "Use toHaveFocus instead of checking activeElement",
61 | },
62 | ],
63 | output: "expect(foo).not.toHaveFocus()",
64 | },
65 | {
66 | code: "expect(window.document.activeElement).toBe(foo)",
67 | errors: [
68 | {
69 | message: "Use toHaveFocus instead of checking activeElement",
70 | },
71 | ],
72 | output: "expect(foo).toHaveFocus()",
73 | },
74 | {
75 | code: "expect(global.window.document.activeElement).toBe(foo)",
76 | errors: [
77 | {
78 | message: "Use toHaveFocus instead of checking activeElement",
79 | },
80 | ],
81 | output: "expect(foo).toHaveFocus()",
82 | },
83 | {
84 | code: "expect(global.document.activeElement).toBe(foo)",
85 | errors: [
86 | {
87 | message: "Use toHaveFocus instead of checking activeElement",
88 | },
89 | ],
90 | output: "expect(foo).toHaveFocus()",
91 | },
92 | {
93 | code: "expect(foo).toBe(global.document.activeElement)",
94 | errors: [
95 | {
96 | message: "Use toHaveFocus instead of checking activeElement",
97 | },
98 | ],
99 | output: "expect(foo).toHaveFocus()",
100 | },
101 | {
102 | code: "expect(foo).toBe(window.document.activeElement)",
103 | errors: [
104 | {
105 | message: "Use toHaveFocus instead of checking activeElement",
106 | },
107 | ],
108 | output: "expect(foo).toHaveFocus()",
109 | },
110 |
111 | {
112 | code: "expect(foo).toBe(global.window.document.activeElement)",
113 | errors: [
114 | {
115 | message: "Use toHaveFocus instead of checking activeElement",
116 | },
117 | ],
118 | output: "expect(foo).toHaveFocus()",
119 | },
120 | {
121 | code: "expect(foo).toBe(document.activeElement)",
122 | errors: [
123 | {
124 | message: "Use toHaveFocus instead of checking activeElement",
125 | },
126 | ],
127 | output: "expect(foo).toHaveFocus()",
128 | },
129 |
130 | {
131 | code: "expect(foo).toEqual(document.activeElement)",
132 | errors: [
133 | {
134 | message: "Use toHaveFocus instead of checking activeElement",
135 | },
136 | ],
137 | output: "expect(foo).toHaveFocus()",
138 | },
139 | ],
140 | });
141 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-prefer-to-have-class.js:
--------------------------------------------------------------------------------
1 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
2 | import * as rule from "../../../rules/prefer-to-have-class";
3 |
4 | const errors = [{ messageId: "use-to-have-class" }];
5 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
6 | ruleTester.run("prefer-to-have-class", rule, {
7 | valid: [
8 | `expect().toBe(true)`,
9 | `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
10 | `const el = screen.getByText("foo"); expect(el.class).toEqual(foo)`,
11 | `const el = screen.getByText("foo"); expect(el).toHaveAttribute("class")`,
12 | `const el = screen.getByText("foo"); expect(el).toHaveAttribute("className", "bar")`,
13 | `const el = screen.getByText("foo"); expect(el).toHaveAttribute("clazz", "bar")`,
14 | `const el = screen.getByText("foo"); expect(el).not.toHaveAttribute("clazz", "bar")`,
15 | `const el = screen.getByText("foo"); expect(el).not.toHaveAttribute("clazz", expect.stringContaining("bar"))`,
16 | `const el = screen.getByText("foo"); expect(el).toHaveAttribute("clazz", expect.stringContaining("bar"))`,
17 | `const el = screen.getByText("foo"); expect(el).toHaveProperty("class", "foo")`,
18 | `const el = screen.getByText("foo"); expect(el).toHaveProperty("clazz", "foo")`,
19 | `const el = screen.getByText("foo"); expect(el).not.toHaveProperty("clazz", "foo")`,
20 | `const el = screen.getByText("foo"); expect(el).toHaveProperty("clazz", expect.stringContaining("bar"))`,
21 | `const el = screen.getByText("foo"); expect(el).not.toHaveProperty("clazz", expect.stringContaining("bar"))`,
22 | `const el = screen.getByText("foo"); expect(el).toHaveAttribute("class", expect.stringMatching("bar"));`,
23 | `const { result } = renderHook(() =>
24 | useMyHook({
25 | classes,
26 | })
27 | );
28 |
29 | expect(result.current.className).toBe("foo");`,
30 | `const { result } = renderHook(() =>
31 | useMyHook({
32 | classes,
33 | })
34 | );
35 |
36 | expect(result.current.className).toEqual(expect.stringContaining("foo"));`,
37 | `const { result } = renderHook(() =>
38 | useMyHook({
39 | classes,
40 | })
41 | );
42 |
43 | expect(result.current.className).not.toBe("foo");`,
44 | `const el = getFoo(); expect(el).toHaveProperty("className", "foo: bar")`,
45 | `const el = getFoo(); expect(el).not.toHaveProperty("className", "foo: bar")`,
46 | `const el = getFoo(); expect(el).toHaveProperty("className",expect.stringContaining("foo"))`,
47 | ],
48 | invalid: [
49 | {
50 | code: `expect(screen.getByRole("button").className).toBe("foo")`,
51 | errors,
52 | output: `expect(screen.getByRole("button")).toHaveClass("foo", { exact: true })`,
53 | },
54 | {
55 | code: `expect(getByRole("button").className).toBe("foo")`,
56 | errors,
57 | output: `expect(getByRole("button")).toHaveClass("foo", { exact: true })`,
58 | },
59 | {
60 | code: `expect(screen.getByRole("button").className).not.toBe("foo")`,
61 | errors,
62 | output: `expect(screen.getByRole("button")).not.toHaveClass("foo", { exact: true })`,
63 | },
64 | {
65 | code: `const el = screen.getByText("foo"); expect(el).toHaveProperty("className", "foo")`,
66 | errors,
67 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
68 | },
69 | {
70 | code: `const el = getByText("foo"); expect(el).toHaveProperty("className", "foo")`,
71 | errors,
72 | output: `const el = getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
73 | },
74 | {
75 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute("class", "foo")`,
76 | errors,
77 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
78 | },
79 | {
80 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute(\`class\`, "foo")`,
81 | errors,
82 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
83 | },
84 | {
85 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute("class", expect.stringContaining("bar"))`,
86 | errors,
87 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
88 | },
89 | {
90 | code: `const el = screen.getByText("foo"); expect(el).toHaveAttribute(\`class\`, expect.stringContaining("bar"))`,
91 | errors,
92 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
93 | },
94 | {
95 | code: `const el = screen.getByText("foo"); expect(el).not.toHaveProperty("className", "foo")`,
96 | errors,
97 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("foo", { exact: true })`,
98 | },
99 | {
100 | code: `const el = screen.getByText("foo"); expect(el).not.toHaveAttribute("class", "foo")`,
101 | errors,
102 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("foo", { exact: true })`,
103 | },
104 | {
105 | code: `const el = screen.getByText("foo"); expect(el.className).toContain("foo")`,
106 | errors,
107 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo")`,
108 | },
109 | {
110 | code: `const el = screen.getByText("foo"); expect(el.className).not.toContain("foo")`,
111 | errors,
112 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("foo")`,
113 | },
114 | {
115 | code: `const el = screen.getByText("foo"); expect(el.className).toBe("foo")`,
116 | errors,
117 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
118 | },
119 | {
120 | code: `const el = screen.getByText("foo"); expect(el.className).toEqual("foo")`,
121 | errors,
122 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
123 | },
124 | {
125 | code: `const el = screen.getByText("foo"); expect(el.className).toStrictEqual("foo")`,
126 | errors,
127 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo", { exact: true })`,
128 | },
129 | {
130 | code: `const el = screen.getByText("foo"); expect(el.className).toEqual(expect.stringContaining("foo"))`,
131 | errors,
132 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo")`,
133 | },
134 | {
135 | code: `const el = screen.getByText("foo"); expect(el.className).toEqual(expect.stringContaining(\`foo\`))`,
136 | errors,
137 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass(\`foo\`)`,
138 | },
139 | {
140 | code: `const el = screen.getByText("foo"); expect(el.className).toStrictEqual(expect.stringContaining("foo"))`,
141 | errors,
142 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo")`,
143 | },
144 | {
145 | code: `const el = screen.getByText("foo"); expect(el.className).toEqual(expect.stringContaining("bar"))`,
146 | errors,
147 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
148 | },
149 | {
150 | code: `const el = screen.getByText("foo"); expect(el.classList).toContain("bar")`,
151 | errors,
152 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
153 | },
154 | {
155 | code: `const el = screen.getByText("foo"); expect(el.classList).toBe("bar")`,
156 | errors,
157 | },
158 | {
159 | code: `const el = screen.getByText("foo"); expect(el.classList[0]).toBe("bar")`,
160 | errors,
161 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("bar")`,
162 | },
163 | {
164 | code: `const el = screen.getByText("foo"); expect(el.classList[0]).not.toBe("bar")`,
165 | errors,
166 | },
167 | {
168 | code: `const el = screen.getByText("foo"); expect(el.classList[0]).toContain(("fo"))`,
169 | errors,
170 | },
171 |
172 | {
173 | code: `const el = screen.getByText("foo"); expect(el.classList).toEqual(expect.objectContaining({0:"foo"}))`,
174 | errors,
175 | },
176 |
177 | {
178 | code: `const el = screen.getByText("foo"); expect(el.classList).toContain(className)`,
179 | errors,
180 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass(className)`,
181 | },
182 | {
183 | code: `const el = screen.getByText("foo"); expect(el.classList).toContain("className")`,
184 | errors,
185 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("className")`,
186 | },
187 |
188 | {
189 | code: `const el = screen.getByText("foo"); expect(el.classList).toContain(foo("bar"))`,
190 | errors,
191 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass(foo("bar"))`,
192 | },
193 |
194 | {
195 | code: `const el = screen.getByText("foo"); expect(el.classList.contains("foo")).toBe(false)`,
196 | errors,
197 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("foo")`,
198 | },
199 |
200 | {
201 | code: `const el = screen.getByText("foo"); expect(el.classList.contains("foo")).toBe(true)`,
202 | errors,
203 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo")`,
204 | },
205 | {
206 | code: `const el = screen.getByText("foo"); expect(el.classList.contains("foo")).toBeTruthy()`,
207 | errors,
208 | output: `const el = screen.getByText("foo"); expect(el).toHaveClass("foo")`,
209 | },
210 | {
211 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toContain("bar")`,
212 | errors,
213 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("bar")`,
214 | },
215 | {
216 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toBe("bar")`,
217 | errors,
218 | },
219 | {
220 | code: `const el = screen.getByText("foo"); expect(el.classList[0]).not.toContain(("fo"))`,
221 | errors,
222 | },
223 |
224 | {
225 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toEqual(expect.objectContaining({0:"foo"}))`,
226 | errors,
227 | },
228 |
229 | {
230 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toContain(className)`,
231 | errors,
232 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass(className)`,
233 | },
234 | {
235 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toContain("className")`,
236 | errors,
237 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass("className")`,
238 | },
239 |
240 | {
241 | code: `const el = screen.getByText("foo"); expect(el.classList).not.toContain(foo("bar"))`,
242 | errors,
243 | output: `const el = screen.getByText("foo"); expect(el).not.toHaveClass(foo("bar"))`,
244 | },
245 | ],
246 | });
247 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-to-have-attribute.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 | /**
3 | * @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
4 | * @author Ben Monro
5 | */
6 |
7 | //------------------------------------------------------------------------------
8 | // Requirements
9 | //------------------------------------------------------------------------------
10 |
11 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
12 | import * as rule from "../../../rules/prefer-to-have-attribute";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
19 | ruleTester.run("prefer-to-have-attribute", rule, {
20 | valid: [
21 | "expect().toBe(true)",
22 | "expect(element.foo).toBeTruthy()",
23 | "expect(element.getAttributeNode()).toBeNull()",
24 | `expect(element.getAttribute('foo')).toBeGreaterThan(2)`,
25 | `expect(element.getAttribute('foo')).toBeLessThan(2)`,
26 | ],
27 |
28 | invalid: [
29 | {
30 | code: `expect(element.getAttribute('foo')).toMatch(/bar/);`,
31 | errors: [
32 | {
33 | message: "Use toHaveAttribute instead of asserting on getAttribute",
34 | },
35 | ],
36 | output: `expect(element).toHaveAttribute('foo', expect.stringMatching(/bar/));`,
37 | },
38 | {
39 | code: `expect(element.getAttribute('foo')).toContain('bar');`,
40 | errors: [
41 | {
42 | message: "Use toHaveAttribute instead of asserting on getAttribute",
43 | },
44 | ],
45 | output: `expect(element).toHaveAttribute('foo', expect.stringContaining('bar'));`,
46 | },
47 | {
48 | code: "expect(element.getAttribute('foo')).toContain(`bar=${encodeURIComponent(baz.id)}`);",
49 | errors: [
50 | {
51 | message: "Use toHaveAttribute instead of asserting on getAttribute",
52 | },
53 | ],
54 | output:
55 | "expect(element).toHaveAttribute('foo', expect.stringContaining(`bar=${encodeURIComponent(baz.id)}`));",
56 | },
57 | {
58 | code: 'expect(element.getAttribute("foo")).toBe("bar")',
59 | errors: [
60 | {
61 | message: "Use toHaveAttribute instead of asserting on getAttribute",
62 | },
63 | ],
64 | output: 'expect(element).toHaveAttribute("foo", "bar")',
65 | },
66 | {
67 | code: `expect(getByText("yes").getAttribute("data-blah")).toBe(expect.stringMatching(/foo/))`,
68 | errors: [
69 | {
70 | message: "Use toHaveAttribute instead of asserting on getAttribute",
71 | },
72 | ],
73 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", expect.stringMatching(/foo/))`,
74 | },
75 | {
76 | code: `expect(getByText("yes").getAttribute("data-blah")).toBe("")`,
77 | errors: [
78 | {
79 | message: "Use toHaveAttribute instead of asserting on getAttribute",
80 | },
81 | ],
82 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", "")`,
83 | },
84 | {
85 | code: `expect(getByText("yes").getAttribute("data-blah")).toBe('')`,
86 | errors: [
87 | {
88 | message: "Use toHaveAttribute instead of asserting on getAttribute",
89 | },
90 | ],
91 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", '')`,
92 | },
93 | {
94 | code: `expect(getByText('foo').hasAttribute('foo')).toBe(null)`,
95 | errors: [
96 | {
97 | message: "Invalid matcher for hasAttribute",
98 | },
99 | ],
100 | output: null,
101 | },
102 | {
103 | code: `expect(getByText('foo').hasAttribute('foo')).toBeNull()`,
104 | errors: [
105 | {
106 | message: "Invalid matcher for hasAttribute",
107 | },
108 | ],
109 | output: null,
110 | },
111 | {
112 | code: `expect(getByText('foo').getAttribute('foo')).toBeDefined()`,
113 | errors: [
114 | {
115 | message: "Invalid matcher for getAttribute",
116 | },
117 | ],
118 | output: null,
119 | },
120 | {
121 | code: `expect(getByText('foo').getAttribute('foo')).toBeUndefined()`,
122 | errors: [
123 | {
124 | message: "Invalid matcher for getAttribute",
125 | },
126 | ],
127 | output: null,
128 | },
129 | {
130 | code: `expect(getByText('foo').hasAttribute('foo')).toBeUndefined()`,
131 | errors: [
132 | {
133 | message: "Invalid matcher for hasAttribute",
134 | },
135 | ],
136 | output: null,
137 | },
138 | {
139 | code: 'expect(element.hasAttribute("foo")).toBeTruthy()',
140 | errors: [
141 | {
142 | message: "Use toHaveAttribute instead of asserting on hasAttribute",
143 | },
144 | ],
145 | output: 'expect(element).toHaveAttribute("foo")',
146 | },
147 | {
148 | code: 'expect(element.hasAttribute("foo")).toBeFalsy()',
149 | errors: [
150 | {
151 | message: "Use toHaveAttribute instead of asserting on hasAttribute",
152 | },
153 | ],
154 | output: 'expect(element).not.toHaveAttribute("foo")',
155 | },
156 | {
157 | code: 'expect(element.hasAttribute("foo")).toBe(true)',
158 | errors: [
159 | {
160 | message: "Use toHaveAttribute instead of asserting on hasAttribute",
161 | },
162 | ],
163 | output: 'expect(element).toHaveAttribute("foo")',
164 | },
165 | {
166 | code: 'expect(element.hasAttribute("foo")).toBe(false)',
167 | errors: [
168 | {
169 | message: "Use toHaveAttribute instead of asserting on hasAttribute",
170 | },
171 | ],
172 | output: 'expect(element).not.toHaveAttribute("foo")',
173 | },
174 | {
175 | code: 'expect(element.hasAttribute("foo")).toEqual(false)',
176 | errors: [
177 | {
178 | message: "Use toHaveAttribute instead of asserting on hasAttribute",
179 | },
180 | ],
181 | output: 'expect(element).not.toHaveAttribute("foo")',
182 | },
183 | {
184 | code: 'expect(element.getAttribute("foo")).toEqual("bar")',
185 | errors: [
186 | {
187 | message: "Use toHaveAttribute instead of asserting on getAttribute",
188 | },
189 | ],
190 | output: 'expect(element).toHaveAttribute("foo", "bar")',
191 | },
192 | {
193 | code: `expect(getByText("yes").getAttribute("data-blah")).toEqual("")`,
194 | errors: [
195 | {
196 | message: "Use toHaveAttribute instead of asserting on getAttribute",
197 | },
198 | ],
199 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", "")`,
200 | },
201 | {
202 | code: `expect(getByText("yes").getAttribute("data-blah")).toEqual('')`,
203 | errors: [
204 | {
205 | message: "Use toHaveAttribute instead of asserting on getAttribute",
206 | },
207 | ],
208 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", '')`,
209 | },
210 | {
211 | code: 'expect(element.getAttribute("foo")).toStrictEqual("bar")',
212 | errors: [
213 | {
214 | message: "Use toHaveAttribute instead of asserting on getAttribute",
215 | },
216 | ],
217 | output: 'expect(element).toHaveAttribute("foo", "bar")',
218 | },
219 | {
220 | code: `expect(getByText("yes").getAttribute("data-blah")).toStrictEqual("")`,
221 | errors: [
222 | {
223 | message: "Use toHaveAttribute instead of asserting on getAttribute",
224 | },
225 | ],
226 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", "")`,
227 | },
228 | {
229 | code: `expect(getByText("yes").getAttribute("data-blah")).toStrictEqual('')`,
230 | errors: [
231 | {
232 | message: "Use toHaveAttribute instead of asserting on getAttribute",
233 | },
234 | ],
235 | output: `expect(getByText("yes")).toHaveAttribute("data-blah", '')`,
236 | },
237 | {
238 | code: 'expect(element.getAttribute("foo")).toBe(null)',
239 | errors: [
240 | {
241 | message: "Use toHaveAttribute instead of asserting on getAttribute",
242 | },
243 | ],
244 | output: 'expect(element).not.toHaveAttribute("foo")',
245 | },
246 | {
247 | code: 'expect(element.getAttribute("foo")).toBeNull()',
248 | errors: [
249 | {
250 | message: "Use toHaveAttribute instead of asserting on getAttribute",
251 | },
252 | ],
253 | output: 'expect(element).not.toHaveAttribute("foo")',
254 | },
255 | ],
256 | });
257 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-to-have-style.js:
--------------------------------------------------------------------------------
1 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
2 | import * as rule from "../../../rules/prefer-to-have-style";
3 |
4 | const errors = [
5 | { message: "Use toHaveStyle instead of asserting on element style" },
6 | ];
7 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
8 | ruleTester.run("prefer-to-have-style", rule, {
9 | valid: [
10 | `expect().toBe(true)`,
11 | `expect(el).toHaveStyle({foo:"bar"})`,
12 | `expect(el.style).toMatchSnapshot()`,
13 | `expect(el.style).toEqual(1)`,
14 | `expect(el.style).toEqual(foo)`,
15 | `expect(el.style[1]).toEqual([])`,
16 | `expect(el.style[1]).toEqual({})`,
17 | `expect(element.style[0]).toBe(new RegExp('reg'));`,
18 | `expect(el).toHaveAttribute("style")`,
19 | `React.useLayoutEffect(() => {
20 | if (foo) {
21 | document.body.setAttribute("style", "foo");
22 | }
23 | }, [foo]);`,
24 | `expect(collapse.style).not.toContain(
25 | expect.objectContaining({
26 | display: 'none',
27 | height: '0px',
28 | })
29 | )`,
30 | ],
31 | invalid: [
32 | {
33 | code: `expect(a.style).toHaveProperty('transform')`,
34 | errors,
35 | },
36 | {
37 | code: `expect(a.style).not.toHaveProperty('transform')`,
38 | errors,
39 | },
40 | {
41 | code: `expect(a.style).not.toHaveProperty(\`\${foo}\`)`,
42 | errors,
43 | },
44 | {
45 | code: `expect(el.style.foo).toBe("bar")`,
46 | errors,
47 | output: `expect(el).toHaveStyle({foo:"bar"})`,
48 | },
49 | {
50 | code: `expect(el.style.foo).not.toBe("bar")`,
51 | errors,
52 | output: `expect(el).not.toHaveStyle({foo:"bar"})`,
53 | },
54 | {
55 | code: "expect(el.style.backgroundImage).toBe(`url(${foo})`)",
56 | errors,
57 | output: "expect(el).toHaveStyle({backgroundImage:`url(${foo})`})",
58 | },
59 | {
60 | code: "expect(el.style.backgroundImage).not.toBe(`url(${foo})`)",
61 | errors,
62 | output: "expect(el).not.toHaveStyle({backgroundImage:`url(${foo})`})",
63 | },
64 | {
65 | code: `expect(el.style).toHaveProperty("background-color", "green")`,
66 | errors,
67 | output: `expect(el).toHaveStyle({backgroundColor: "green"})`,
68 | },
69 | {
70 | code: `expect(el.style).not.toHaveProperty("background-color", "green")`,
71 | errors,
72 | output: `expect(el).not.toHaveStyle({backgroundColor: "green"})`,
73 | },
74 | {
75 | code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory")`,
76 | errors,
77 | output: `expect(screen.getByTestId("foo")).toHaveStyle({scrollSnapType: "x mandatory"})`,
78 | },
79 | {
80 | code: 'expect(el.style["scroll-snap-type"]).toBe(`${x} mandatory`)',
81 | errors,
82 | output: "expect(el).toHaveStyle({scrollSnapType: `${x} mandatory`})",
83 | },
84 | {
85 | code: `expect(el.style["scroll-snap-type"]).not.toBe("x mandatory")`,
86 | errors,
87 | output: `expect(el).not.toHaveStyle({scrollSnapType: "x mandatory"})`,
88 | },
89 | {
90 | code: `expect(el.style).toContain("background-color")`,
91 | errors,
92 | output: `expect(el).toHaveStyle({backgroundColor: expect.anything()})`,
93 | },
94 | {
95 | code: `expect(el.style).toContain(\`background-color\`)`,
96 | errors,
97 | output: `expect(el).toHaveStyle(\`background-color\`)`,
98 | },
99 | {
100 | code: `expect(el.style).not.toContain(\`background-color\`)`,
101 | errors,
102 | output: `expect(el).not.toHaveStyle(\`background-color\`)`,
103 | },
104 | {
105 | code: `expect(el.style).not.toContain("background-color")`,
106 | errors,
107 | output: `expect(el).not.toHaveStyle({backgroundColor: expect.anything()})`,
108 | },
109 | {
110 | code: `expect(el).toHaveAttribute("style", "background-color: green; border-width: 10px; color: blue;")`,
111 | errors,
112 | output: `expect(el).toHaveStyle("background-color: green; border-width: 10px; color: blue;")`,
113 | },
114 | {
115 | code: `expect(imageElement.style[\`box-shadow\`]).toBe(\`inset 0px 0px 0px 400px \${c}\`)`,
116 | errors,
117 | output: `expect(imageElement).toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px \${c}\`)`,
118 | },
119 | {
120 | code: `expect(imageElement.style[\`box-shadow\` ]).toBe( \`inset 0px 0px 0px 400px \${c}\`)`,
121 | errors,
122 | output: `expect(imageElement).toHaveStyle( \`box-shadow: inset 0px 0px 0px 400px \${c}\`)`,
123 | },
124 | {
125 | code: `expect(imageElement.style[\`box-\${shadow}\`]).toBe("inset 0px 0px 0px 400px 40px")`,
126 | errors,
127 | output: `expect(imageElement).toHaveStyle(\`box-\${shadow}: inset 0px 0px 0px 400px 40px\`)`,
128 | },
129 | {
130 | code: `expect(imageElement.style[\`box-shadow\`]).not.toBe(\`inset 0px 0px 0px 400px \${c}\`)`,
131 | errors,
132 | output: `expect(imageElement).not.toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px \${c}\`)`,
133 | },
134 | {
135 | code: `expect(imageElement.style[\`box-shadow\`]).not.toBe("inset 0px 0px 0px 400px 40px")`,
136 | errors,
137 | output: `expect(imageElement).not.toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px 40px\`)`,
138 | },
139 | {
140 | code: `expect(element.style[1]).toEqual('padding');`,
141 | errors,
142 | output: `expect(element).toHaveStyle({padding: expect.anything()});`,
143 | },
144 | {
145 | code: `expect(element.style[1]).toBe(\`padding\`);`,
146 | errors,
147 | output: `expect(element).toHaveStyle({[\`padding\`]: expect.anything()});`,
148 | },
149 | {
150 | code: `expect(element.style[1]).not.toEqual('padding');`,
151 | errors,
152 | },
153 | {
154 | code: `expect(element.style[1]).not.toBe(\`padding\`);`,
155 | errors,
156 | },
157 | {
158 | code: `expect(element.style[1]).toBe(x);`,
159 | errors,
160 | output: `expect(element).toHaveStyle({[x]: expect.anything()});`,
161 | },
162 | {
163 | code: `expect(element.style[0]).toBe(1);`,
164 | errors,
165 | },
166 | {
167 | code: `expect(element.style[0]).toBe(/RegExp/);`,
168 | errors,
169 | },
170 | {
171 | code: `expect(imageElement.style[computed]).toBe(\`inset 0px 0px 0px 400px \${c}\`)`,
172 | errors,
173 | output: null,
174 | },
175 | {
176 | code: `expect(imageElement.style[computed]).not.toBe(\`inset 0px 0px 0px 400px \${c}\`)`,
177 | errors,
178 | output: null,
179 | },
180 | {
181 | code: `
182 | expect(myStencil({color: '--my-var'}).style).toHaveProperty(
183 | myStencil.vars.color,
184 | 'var(--my-var)'
185 | );
186 | `,
187 | errors,
188 | output: `
189 | expect(myStencil({color: '--my-var'})).toHaveStyle(
190 | {[myStencil.vars.color]: 'var(--my-var)'}
191 | );
192 | `,
193 | },
194 | {
195 | code: `
196 | expect(myStencil({color: '--my-var'}).style).not.toHaveProperty(
197 | myStencil.vars.color,
198 | 'var(--my-var)'
199 | );
200 | `,
201 | errors,
202 | output: `
203 | expect(myStencil({color: '--my-var'})).not.toHaveStyle(
204 | {[myStencil.vars.color]: 'var(--my-var)'}
205 | );
206 | `,
207 | },
208 | ],
209 | });
210 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-to-have-text-content.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 | /**
3 | * @fileoverview Prefer toHaveTextContent over checking element.textContent
4 | * @author Ben Monro
5 | */
6 |
7 | //------------------------------------------------------------------------------
8 | // Requirements
9 | //------------------------------------------------------------------------------
10 |
11 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
12 | import * as rule from "../../../rules/prefer-to-have-text-content";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
19 | ruleTester.run("prefer-to-have-text-content", rule, {
20 | valid: [
21 | `expect().toBe(true)`,
22 | `expect(string).toBe("foo")`,
23 | `expect(element).toHaveTextContent("foo")`,
24 | `expect(container.lastNode).toBe("foo")`,
25 | ],
26 |
27 | invalid: [
28 | {
29 | code: 'expect(element.textContent).toBe("foo")',
30 | errors: [
31 | {
32 | message:
33 | "Use toHaveTextContent instead of asserting on DOM node attributes",
34 | },
35 | ],
36 | output: `expect(element).toHaveTextContent("foo")`,
37 | },
38 | {
39 | code: 'expect(element.textContent).not.toBe("foo")',
40 | errors: [
41 | {
42 | message:
43 | "Use toHaveTextContent instead of asserting on DOM node attributes",
44 | },
45 | ],
46 | output: `expect(element).not.toHaveTextContent("foo")`,
47 | },
48 | {
49 | code: 'expect(screen.getByText("foo").textContent).toBe("foo")',
50 | errors: [
51 | {
52 | message:
53 | "Use toHaveTextContent instead of asserting on DOM node attributes",
54 | },
55 | ],
56 | output: `expect(screen.getByText("foo")).toHaveTextContent("foo")`,
57 | },
58 | {
59 | code: 'expect(container.firstChild.textContent).toBe("foo")',
60 | errors: [
61 | {
62 | message:
63 | "Use toHaveTextContent instead of asserting on DOM node attributes",
64 | },
65 | ],
66 | output: `expect(container.firstChild).toHaveTextContent("foo")`,
67 | },
68 | {
69 | code: 'expect(element.textContent).toEqual("foo")',
70 | errors: [
71 | {
72 | message:
73 | "Use toHaveTextContent instead of asserting on DOM node attributes",
74 | },
75 | ],
76 | output: `expect(element).toHaveTextContent("foo")`,
77 | },
78 | {
79 | code: 'expect(element.textContent).toContain("foo")',
80 | errors: [
81 | {
82 | message:
83 | "Use toHaveTextContent instead of asserting on DOM node attributes",
84 | },
85 | ],
86 | output: `expect(element).toHaveTextContent(/foo/)`,
87 | },
88 | {
89 | code: 'expect(element.textContent).toContain("$42/month?")',
90 | errors: [
91 | {
92 | message:
93 | "Use toHaveTextContent instead of asserting on DOM node attributes",
94 | },
95 | ],
96 | output: "expect(element).toHaveTextContent(/\\$42\\/month\\?/)",
97 | },
98 | {
99 | code: "expect(element.textContent).toContain(100)",
100 | errors: [
101 | {
102 | message:
103 | "Use toHaveTextContent instead of asserting on DOM node attributes",
104 | },
105 | ],
106 | output: `expect(element).toHaveTextContent(/100/)`,
107 | },
108 | {
109 | code: 'expect(container.firstChild.textContent).toContain("foo")',
110 | errors: [
111 | {
112 | message:
113 | "Use toHaveTextContent instead of asserting on DOM node attributes",
114 | },
115 | ],
116 | output: `expect(container.firstChild).toHaveTextContent(/foo/)`,
117 | },
118 | {
119 | code: `expect(container.textContent).toContain(FOO.bar)`,
120 | errors: [
121 | {
122 | message:
123 | "Use toHaveTextContent instead of asserting on DOM node attributes",
124 | },
125 | ],
126 | output: `expect(container).toHaveTextContent(new RegExp(FOO.bar))`,
127 | },
128 | {
129 | code: `expect(container.textContent).not.toContain(FOO.bar)`,
130 | errors: [
131 | {
132 | message:
133 | "Use toHaveTextContent instead of asserting on DOM node attributes",
134 | },
135 | ],
136 | output: `expect(container).not.toHaveTextContent(new RegExp(FOO.bar))`,
137 | },
138 | {
139 | code: "expect(container.textContent).toContain(`${FOO.bar} baz`)",
140 | errors: [
141 | {
142 | message:
143 | "Use toHaveTextContent instead of asserting on DOM node attributes",
144 | },
145 | ],
146 | output:
147 | "expect(container).toHaveTextContent(new RegExp(`${FOO.bar} baz`))",
148 | },
149 | {
150 | code: `expect(container.textContent).toContain(bazify(FOO.bar))`,
151 | errors: [
152 | {
153 | message:
154 | "Use toHaveTextContent instead of asserting on DOM node attributes",
155 | },
156 | ],
157 | output: `expect(container).toHaveTextContent(new RegExp(bazify(FOO.bar)))`,
158 | },
159 | {
160 | code: 'expect(element.textContent).toMatch("foo")',
161 | errors: [
162 | {
163 | message:
164 | "Use toHaveTextContent instead of asserting on DOM node attributes",
165 | },
166 | ],
167 | output: `expect(element).toHaveTextContent(/foo/)`,
168 | },
169 | {
170 | code: "expect(element.textContent).toMatch(/foo bar/)",
171 | errors: [
172 | {
173 | message:
174 | "Use toHaveTextContent instead of asserting on DOM node attributes",
175 | },
176 | ],
177 | output: "expect(element).toHaveTextContent(/foo bar/)",
178 | },
179 | {
180 | code: "expect(element.textContent).not.toMatch(/foo bar/)",
181 | errors: [
182 | {
183 | message:
184 | "Use toHaveTextContent instead of asserting on DOM node attributes",
185 | },
186 | ],
187 | output: "expect(element).not.toHaveTextContent(/foo bar/)",
188 | },
189 | {
190 | code: 'expect(element.textContent).not.toMatch("foo")',
191 | errors: [
192 | {
193 | message:
194 | "Use toHaveTextContent instead of asserting on DOM node attributes",
195 | },
196 | ],
197 | output: `expect(element).not.toHaveTextContent(/foo/)`,
198 | },
199 | {
200 | code: 'expect(element.textContent).not.toMatch("$42/month?")',
201 | errors: [
202 | {
203 | message:
204 | "Use toHaveTextContent instead of asserting on DOM node attributes",
205 | },
206 | ],
207 | output: `expect(element).not.toHaveTextContent(/\\$42\\/month\\?/)`,
208 | },
209 | ],
210 | });
211 |
--------------------------------------------------------------------------------
/src/__tests__/lib/rules/prefer-to-have-value.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 | /**
3 | * @fileoverview Prefer toBeEmptyDOMElement over checking innerHTML
4 | * @author Ben Monro
5 | */
6 |
7 | //------------------------------------------------------------------------------
8 | // Requirements
9 | //------------------------------------------------------------------------------
10 |
11 | import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
12 | import * as rule from "../../../rules/prefer-to-have-value";
13 |
14 | //------------------------------------------------------------------------------
15 | // Tests
16 | //------------------------------------------------------------------------------
17 |
18 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } });
19 |
20 | const errors = [{ messageId: "use-to-have-value" }];
21 | ruleTester.run("prefer-to-have-value", rule, {
22 | valid: [
23 | `expect().toBe(true)`,
24 | `expect(screen.getByRole("radio").value).toEqual("foo")`,
25 | `expect(screen.queryAllByRole("checkbox")[0].value).toStrictEqual("foo")`,
26 | `async function x() { expect((await screen.findByRole("button")).value).toBe("foo") }`,
27 |
28 | `expect(element).toHaveValue('foo')`,
29 | `expect(element.value).toBeGreaterThan(2);`,
30 | `expect(element.value).toBeLessThan(2);`,
31 |
32 | `const element = document.getElementById('asdfasf');
33 | expect(element.value).toEqual('foo');`,
34 |
35 | `let element;
36 | element = someOtherFunction();
37 | expect(element.value).toStrictEqual('foo');`,
38 |
39 | `const element = { value: 'foo' };
40 | expect(element.value).toBe('foo');`,
41 |
42 | `expect(screen.getByRole("radio").value).not.toEqual("foo")`,
43 | `expect(screen.queryAllByRole("checkbox")[0].value).not.toStrictEqual("foo")`,
44 | `async function x() { expect((await screen.findByRole("button")).value).not.toBe("foo") }`,
45 |
46 | `const element = document.getElementById('asdfasf');
47 | expect(element.value).not.toEqual('foo');`,
48 |
49 | `let element;
50 | element = someOtherFunction();
51 | expect(element.value).not.toStrictEqual('foo');`,
52 |
53 | `const element = { value: 'foo' };
54 | expect(element.value).not.toBe('foo');`,
55 | `
56 | const res = makePath()();
57 | expect(res.value).toEqual('/repositories/create');
58 | `,
59 | ],
60 | invalid: [
61 | {
62 | code: `expect(element).toHaveAttribute('value', 'foo')`,
63 | errors,
64 | output: `expect(element).toHaveValue('foo')`,
65 | },
66 | {
67 | code: `expect(element).toHaveProperty("value", "foo")`,
68 | errors,
69 | output: `expect(element).toHaveValue("foo")`,
70 | },
71 | {
72 | code: `expect(element).not.toHaveAttribute('value', 'foo')`,
73 | errors,
74 | output: `expect(element).not.toHaveValue('foo')`,
75 | },
76 | {
77 | code: `expect(element).not.toHaveProperty("value", "foo")`,
78 | errors,
79 | output: `expect(element).not.toHaveValue("foo")`,
80 | },
81 | //==========================================================================
82 | {
83 | code: `expect(screen.getByRole("textbox").value).toEqual("foo")`,
84 | errors,
85 | output: `expect(screen.getByRole("textbox")).toHaveValue("foo")`,
86 | },
87 | {
88 | code: `expect(screen.queryByRole("dropdown").value).toEqual("foo")`,
89 | errors,
90 | output: `expect(screen.queryByRole("dropdown")).toHaveValue("foo")`,
91 | },
92 | {
93 | code: `async function x() { expect((await screen.findByRole("textbox")).value).toEqual("foo") }`,
94 | errors,
95 | output: `async function x() { expect((await screen.findByRole("textbox"))).toHaveValue("foo") }`,
96 | },
97 | {
98 | code: `const element = screen.getByRole("textbox"); expect(element.value).toBe("foo");`,
99 | errors,
100 | output: `const element = screen.getByRole("textbox"); expect(element).toHaveValue("foo");`,
101 | },
102 | {
103 | code: `expect(screen.getByRole("textbox").value).not.toEqual("foo")`,
104 | errors,
105 | output: `expect(screen.getByRole("textbox")).not.toHaveValue("foo")`,
106 | },
107 | {
108 | code: `expect(screen.queryByRole("dropdown").value).not.toEqual("foo")`,
109 | errors,
110 | output: `expect(screen.queryByRole("dropdown")).not.toHaveValue("foo")`,
111 | },
112 | {
113 | code: `async function x() { expect((await screen.getByRole("textbox")).value).not.toEqual("foo") }`,
114 | errors,
115 | output: `async function x() { expect((await screen.getByRole("textbox"))).not.toHaveValue("foo") }`,
116 | },
117 | {
118 | code: `const element = screen.getByRole("textbox"); expect(element.value).not.toBe("foo");`,
119 | errors,
120 | output: `const element = screen.getByRole("textbox"); expect(element).not.toHaveValue("foo");`,
121 | },
122 | ],
123 | });
124 |
--------------------------------------------------------------------------------
/src/__tests__/queries.test.js:
--------------------------------------------------------------------------------
1 | const TestingLibraryDomRef = { throwWhenRequiring: false };
2 |
3 | const requireQueries = (throwWhenRequiring) => {
4 | jest.resetModules();
5 |
6 | TestingLibraryDomRef.throwWhenRequiring = throwWhenRequiring;
7 |
8 | return require("../queries");
9 | };
10 |
11 | jest.mock("@testing-library/dom", () => {
12 | if (TestingLibraryDomRef.throwWhenRequiring) {
13 | throw new (class extends Error {
14 | constructor(message) {
15 | super(message);
16 | this.code = "MODULE_NOT_FOUND";
17 | }
18 | })();
19 | }
20 |
21 | return jest.requireActual("@testing-library/dom");
22 | });
23 |
24 | describe("when @testing-library/dom is not available", () => {
25 | it("uses the default queries", () => {
26 | const { queries } = requireQueries(true);
27 |
28 | expect([...queries].sort()).toStrictEqual([
29 | "findAllByAltText",
30 | "findAllByDisplayValue",
31 | "findAllByLabelText",
32 | "findAllByPlaceholderText",
33 | "findAllByRole",
34 | "findAllByTestId",
35 | "findAllByText",
36 | "findAllByTitle",
37 | "findByAltText",
38 | "findByDisplayValue",
39 | "findByLabelText",
40 | "findByPlaceholderText",
41 | "findByRole",
42 | "findByTestId",
43 | "findByText",
44 | "findByTitle",
45 | "getAllByAltText",
46 | "getAllByDisplayValue",
47 | "getAllByLabelText",
48 | "getAllByPlaceholderText",
49 | "getAllByRole",
50 | "getAllByTestId",
51 | "getAllByText",
52 | "getAllByTitle",
53 | "getByAltText",
54 | "getByDisplayValue",
55 | "getByLabelText",
56 | "getByPlaceholderText",
57 | "getByRole",
58 | "getByTestId",
59 | "getByText",
60 | "getByTitle",
61 | "queryAllByAltText",
62 | "queryAllByDisplayValue",
63 | "queryAllByLabelText",
64 | "queryAllByPlaceholderText",
65 | "queryAllByRole",
66 | "queryAllByTestId",
67 | "queryAllByText",
68 | "queryAllByTitle",
69 | "queryByAltText",
70 | "queryByDisplayValue",
71 | "queryByLabelText",
72 | "queryByPlaceholderText",
73 | "queryByRole",
74 | "queryByTestId",
75 | "queryByText",
76 | "queryByTitle",
77 | ]);
78 | });
79 | });
80 |
81 | describe("when @testing-library/dom is available", () => {
82 | it("returns the queries from the library", () => {
83 | const { queries } = requireQueries(false);
84 |
85 | expect([...queries].sort()).toStrictEqual([
86 | "findAllByAltText",
87 | "findAllByDisplayValue",
88 | "findAllByLabelText",
89 | "findAllByPlaceholderText",
90 | "findAllByRole",
91 | "findAllByTestId",
92 | "findAllByText",
93 | "findAllByTitle",
94 | "findByAltText",
95 | "findByDisplayValue",
96 | "findByLabelText",
97 | "findByPlaceholderText",
98 | "findByRole",
99 | "findByTestId",
100 | "findByText",
101 | "findByTitle",
102 | "getAllByAltText",
103 | "getAllByDisplayValue",
104 | "getAllByLabelText",
105 | "getAllByPlaceholderText",
106 | "getAllByRole",
107 | "getAllByTestId",
108 | "getAllByText",
109 | "getAllByTitle",
110 | "getByAltText",
111 | "getByDisplayValue",
112 | "getByLabelText",
113 | "getByPlaceholderText",
114 | "getByRole",
115 | "getByTestId",
116 | "getByText",
117 | "getByTitle",
118 | "queryAllByAltText",
119 | "queryAllByDisplayValue",
120 | "queryAllByLabelText",
121 | "queryAllByPlaceholderText",
122 | "queryAllByRole",
123 | "queryAllByTestId",
124 | "queryAllByText",
125 | "queryAllByTitle",
126 | "queryByAltText",
127 | "queryByDisplayValue",
128 | "queryByLabelText",
129 | "queryByPlaceholderText",
130 | "queryByRole",
131 | "queryByTestId",
132 | "queryByText",
133 | "queryByTitle",
134 | ]);
135 | });
136 |
137 | it("re-throws unexpected errors", () => {
138 | jest.mock("@testing-library/dom", () => {
139 | throw new Error("oh noes!");
140 | });
141 |
142 | jest.resetModules();
143 |
144 | expect(() => require("../queries")).toThrow(/oh noes!/iu);
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/src/__tests__/rule-tester.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jest/no-export */
2 |
3 | import { RuleTester } from 'eslint';
4 | import semver from 'semver';
5 | import { version as eslintVersion } from 'eslint/package.json';
6 |
7 | // we need to have a test as kcd-scripts doesn't let us
8 | // exclude this file from being run via jest as a test
9 | it('is true', () => {
10 | expect(true).toBe(true);
11 | });
12 |
13 | export const usingFlatConfig = semver.major(eslintVersion) >= 9;
14 |
15 | export class FlatCompatRuleTester extends RuleTester {
16 | constructor(testerConfig) {
17 | super(FlatCompatRuleTester._flatCompat(testerConfig));
18 | }
19 |
20 | run(
21 | ruleName,
22 | rule,
23 | tests,
24 | ) {
25 | super.run(ruleName, rule, {
26 | valid: tests.valid.map(t => FlatCompatRuleTester._flatCompat(t)),
27 | invalid: tests.invalid.map(t => FlatCompatRuleTester._flatCompat(t)),
28 | });
29 | }
30 |
31 | static _flatCompat(config) {
32 | if (!config || !usingFlatConfig || typeof config === 'string') {
33 | return config;
34 | }
35 |
36 | const obj = {
37 | languageOptions: { parserOptions: {} },
38 | };
39 |
40 | for (const [key, value] of Object.entries(config)) {
41 | if (key === 'parser') {
42 | obj.languageOptions.parser = require(value);
43 |
44 | continue;
45 | }
46 |
47 | if (key === 'parserOptions') {
48 | for (const [option, val] of Object.entries(value)) {
49 | if (option === 'ecmaVersion' || option === 'sourceType') {
50 | obj.languageOptions[option] = val
51 |
52 | continue;
53 | }
54 |
55 | obj.languageOptions.parserOptions[option] = val;
56 | }
57 |
58 | continue;
59 | }
60 |
61 | obj[key] = value;
62 | }
63 |
64 | return obj;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/assignment-ast.js:
--------------------------------------------------------------------------------
1 | import { queries } from "./queries";
2 | import { getScope } from './context';
3 |
4 | /**
5 | * Gets the inner relevant node (CallExpression, Identity, et al.) given a generic expression node
6 | * await someAsyncFunc() => someAsyncFunc()
7 | * someElement as HTMLDivElement => someElement
8 | *
9 | * @param {Object} context - Context for a rule
10 | * @param {Object} node - Node for a rule
11 | * @param {Object} expression - An expression node
12 | * @returns {Object} - A node
13 | */
14 | export function getInnerNodeFrom(context, node, expression) {
15 | switch (expression.type) {
16 | case "Identifier":
17 | return getAssignmentForIdentifier(context, node, expression.name);
18 | case "TSAsExpression":
19 | return getInnerNodeFrom(context, node, expression.expression);
20 | case "AwaitExpression":
21 | return getInnerNodeFrom(context, node, expression.argument);
22 | case "MemberExpression":
23 | return getInnerNodeFrom(context, node, expression.object);
24 | default:
25 | return expression;
26 | }
27 | }
28 |
29 | /**
30 | * Get the node corresponding to the latest assignment to a variable named `identifierName`
31 | *
32 | * @param {Object} context - Context for a rule
33 | * @param {Object} node - Node for a rule
34 | * @param {String} identifierName - Name of an identifier
35 | * @returns {Object} - A node, possibly undefined
36 | */
37 | export function getAssignmentForIdentifier(context, node, identifierName) {
38 | const variable = getScope(context, node).set.get(identifierName);
39 |
40 | if (!variable) return;
41 | const init = variable.defs[0].node.init;
42 |
43 | let assignmentNode;
44 | if (init) {
45 | // let foo = bar;
46 | assignmentNode = getInnerNodeFrom(context, node, init);
47 | } else {
48 | // let foo;
49 | // foo = bar;
50 | const assignmentRef = variable.references
51 | .reverse()
52 | .find((ref) => !!ref.writeExpr);
53 | if (!assignmentRef) {
54 | return;
55 | }
56 | assignmentNode = getInnerNodeFrom(context, node, assignmentRef.writeExpr);
57 | }
58 | return assignmentNode;
59 | }
60 |
61 | /**
62 | * get query node, arg and isDTLQuery flag for a given node. useful for rules that you only
63 | * want to apply to dom elements.
64 | *
65 | * @param {Object} context - Context for a rule
66 | * @param {Object} nodeWithValueProp - AST Node to get the query from
67 | * @returns {Object} - Object with query, queryArg & isDTLQuery
68 | */
69 | export function getQueryNodeFrom(context, nodeWithValueProp) {
70 | const queryNode = getInnerNodeFrom(context, nodeWithValueProp, nodeWithValueProp);
71 |
72 | if (!queryNode || !queryNode.callee) {
73 | return {
74 | isDTLQuery: false,
75 | query: null,
76 | queryArg: null,
77 | };
78 | }
79 |
80 | const query =
81 | queryNode.callee.name ||
82 | (queryNode.callee.property && queryNode.callee.property.name);
83 | const queryArg = queryNode.arguments[0] && queryNode.arguments[0].value;
84 | const isDTLQuery = queries.includes(query);
85 |
86 | return { queryArg, query, isDTLQuery };
87 | }
88 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore next */
2 | export function getSourceCode(context) {
3 | if ('sourceCode' in context) {
4 | return context.sourceCode;
5 | }
6 |
7 | return context.getSourceCode();
8 | }
9 |
10 | /* istanbul ignore next */
11 | export function getScope(context, node) {
12 | const sourceCode = getSourceCode(context);
13 |
14 | if (sourceCode && sourceCode.getScope) {
15 | return sourceCode.getScope(node);
16 | }
17 |
18 | return context.getScope();
19 | }
20 |
--------------------------------------------------------------------------------
/src/createBannedAttributeRule.js:
--------------------------------------------------------------------------------
1 | import { getQueryNodeFrom } from "./assignment-ast";
2 |
3 | export default ({ preferred, negatedPreferred, attributes }) => (context) => {
4 | const getCorrectFunctionFor = (node, negated = false) =>
5 | (node.arguments.length === 1 ||
6 | node.arguments[1].value === true ||
7 | node.arguments[1].type !== "Literal" ||
8 | (typeof node.arguments[1].value === "string" &&
9 | node.arguments[1].value.toLowerCase() === "true") ||
10 | node.arguments[1].value === "") &&
11 | !negated
12 | ? preferred
13 | : negatedPreferred;
14 |
15 | const isBannedArg = (node) =>
16 | node.arguments.length &&
17 | attributes.some((attr) => attr === node.arguments[0].value);
18 |
19 | //expect(el).not.toBeEnabled() => expect(el).toBeDisabled()
20 | return {
21 | [`CallExpression[callee.property.name=/${preferred}|${negatedPreferred}/][callee.object.property.name='not'][callee.object.object.callee.name='expect']`](
22 | node
23 | ) {
24 | if (!negatedPreferred.startsWith("toBe")) {
25 | return;
26 | }
27 |
28 | const incorrectFunction = node.callee.property.name;
29 |
30 | const correctFunction =
31 | incorrectFunction === preferred ? negatedPreferred : preferred;
32 | context.report({
33 | message: `Use ${correctFunction}() instead of not.${incorrectFunction}()`,
34 | node,
35 | fix: (fixer) =>
36 | fixer.replaceTextRange(
37 | [node.callee.object.property.range[0], node.range[1]],
38 | `${correctFunction}()`
39 | ),
40 | });
41 | },
42 | //expect(getByText('foo').).toBeTruthy()
43 | "CallExpression[callee.property.name=/toBe(Truthy|Falsy)?|toEqual/][callee.object.callee.name='expect']"(
44 | node
45 | ) {
46 | if (!node.callee.object.arguments.length) {
47 | return;
48 | }
49 |
50 | const {
51 | arguments: [{ object, property, property: { name } = {} }],
52 | } = node.callee.object;
53 | const matcher = node.callee.property.name;
54 | const matcherArg = node.arguments.length && node.arguments[0].value;
55 | if (!attributes.some((attr) => attr === name)) {
56 | return;
57 | }
58 | const { isDTLQuery } = getQueryNodeFrom(
59 | context,
60 | node.callee.object.arguments[0]
61 | );
62 | if (!isDTLQuery) return;
63 | const isNegated =
64 | matcher.endsWith("Falsy") ||
65 | ((matcher === "toBe" || matcher === "toEqual") && matcherArg !== true);
66 | const correctFunction = getCorrectFunctionFor(
67 | node.callee.object,
68 | isNegated
69 | );
70 | context.report({
71 | node,
72 | message: `Use ${correctFunction}() instead of checking .${name} directly`,
73 | fix: (fixer) => [
74 | fixer.removeRange([object.range[1], property.range[1]]),
75 | fixer.replaceTextRange(
76 | [node.callee.property.range[0], node.range[1]],
77 | `${correctFunction}()`
78 | ),
79 | ],
80 | });
81 | },
82 | "CallExpression[callee.property.name=/toHaveProperty|toHaveAttribute/][callee.object.property.name='not'][callee.object.object.callee.name='expect']"(
83 | node
84 | ) {
85 | if (!isBannedArg(node)) {
86 | return;
87 | }
88 |
89 | const arg = node.arguments[0].value;
90 | const correctFunction = getCorrectFunctionFor(node, true);
91 |
92 | const incorrectFunction = node.callee.property.name;
93 | context.report({
94 | message: `Use ${correctFunction}() instead of not.${incorrectFunction}('${arg}')`,
95 | node,
96 | fix: (fixer) =>
97 | fixer.replaceTextRange(
98 | [node.callee.object.property.range[0], node.range[1]],
99 | `${correctFunction}()`
100 | ),
101 | });
102 | },
103 | "CallExpression[callee.object.callee.name='expect'][callee.property.name=/toHaveProperty|toHaveAttribute/]"(
104 | node
105 | ) {
106 | if (!isBannedArg(node)) {
107 | return;
108 | }
109 | const { isDTLQuery } = getQueryNodeFrom(
110 | context,
111 | node.callee.object.arguments[0]
112 | );
113 |
114 | if (!isDTLQuery) return;
115 | const correctFunction = getCorrectFunctionFor(node);
116 |
117 | const incorrectFunction = node.callee.property.name;
118 |
119 | const message = `Use ${correctFunction}() instead of ${incorrectFunction}(${node.arguments
120 | .map(({ raw, name }) => raw || name)
121 | .join(", ")})`;
122 |
123 | const secondArgIsLiteral =
124 | node.arguments.length === 2 && node.arguments[1].type === "Literal";
125 |
126 | context.report({
127 | node: node.callee.property,
128 | message,
129 | fix: (fixer) => {
130 | if (node.arguments.length === 1 || secondArgIsLiteral) {
131 | return [
132 | fixer.replaceTextRange(
133 | [node.callee.property.range[0], node.range[1]],
134 | `${correctFunction}()`
135 | ),
136 | ];
137 | }
138 |
139 | return null;
140 | },
141 | });
142 | },
143 | };
144 | };
145 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview lint rules for use with jest-dom
3 | * @author Ben Monro
4 | */
5 |
6 | //------------------------------------------------------------------------------
7 | // Requirements
8 | //------------------------------------------------------------------------------
9 |
10 | import requireIndex from "requireindex";
11 | import {
12 | name as packageName,
13 | version as packageVersion,
14 | } from "../package.json";
15 |
16 | //------------------------------------------------------------------------------
17 | // Plugin Definition
18 | //------------------------------------------------------------------------------
19 |
20 | // import all rules in src/rules and re-export them for .eslintrc configs
21 | export const rules = requireIndex(`${__dirname}/rules`);
22 |
23 | const allRules = Object.entries(rules).reduce(
24 | (memo, [name]) => ({
25 | ...memo,
26 | ...{ [`jest-dom/${name}`]: "error" },
27 | }),
28 | {}
29 | );
30 |
31 | const recommendedRules = allRules;
32 |
33 | const plugin = {
34 | meta: {
35 | name: packageName,
36 | version: packageVersion,
37 | },
38 | configs: {
39 | recommended: {
40 | plugins: ["jest-dom"],
41 | rules: recommendedRules,
42 | },
43 | all: {
44 | plugins: ["jest-dom"],
45 | rules: allRules,
46 | },
47 | },
48 | rules,
49 | };
50 |
51 | plugin.configs["flat/recommended"] = {
52 | plugins: { "jest-dom": plugin },
53 | rules: recommendedRules,
54 | };
55 | plugin.configs["flat/all"] = {
56 | plugins: { "jest-dom": plugin },
57 | rules: allRules,
58 | };
59 |
60 | export default plugin;
61 |
62 | // explicitly export config to allow using this plugin in CJS-based
63 | // eslint.config.js files without needing to deal with the .default
64 | // and also retain backwards compatibility with `.eslintrc` configs
65 | export const configs = plugin.configs;
66 |
--------------------------------------------------------------------------------
/src/queries.js:
--------------------------------------------------------------------------------
1 | let theQueries = [
2 | "findAllBy",
3 | "findBy",
4 | "getAllBy",
5 | "getBy",
6 | "queryAllBy",
7 | "queryBy",
8 | ].flatMap((prefix) =>
9 | [
10 | "AltText",
11 | "DisplayValue",
12 | "LabelText",
13 | "PlaceholderText",
14 | "Role",
15 | "TestId",
16 | "Text",
17 | "Title",
18 | ].map((element) => `${prefix}${element}`)
19 | );
20 |
21 | (() => {
22 | try {
23 | const { queries: allQueries } = require("@testing-library/dom");
24 |
25 | theQueries = Object.keys(allQueries);
26 | } catch (error) {
27 | if (error.code !== "MODULE_NOT_FOUND") {
28 | throw error;
29 | }
30 | }
31 | })();
32 |
33 | export const queries = theQueries;
34 |
--------------------------------------------------------------------------------
/src/rules/prefer-checked.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
3 | * @author Ben Monro
4 | */
5 |
6 | import createBannedAttributeRule from "../createBannedAttributeRule";
7 |
8 | export const meta = {
9 | docs: {
10 | description: "prefer toBeChecked over checking attributes",
11 | category: "Best Practices",
12 | recommended: true,
13 | url: "prefer-checked",
14 | },
15 | fixable: "code",
16 | };
17 |
18 | export const create = createBannedAttributeRule({
19 | preferred: "toBeChecked",
20 | negatedPreferred: "not.toBeChecked",
21 | attributes: ["checked", "aria-checked"],
22 | });
23 |
--------------------------------------------------------------------------------
/src/rules/prefer-empty.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Prefer toBeEmpty over checking innerHTML
3 | * @author Ben Monro
4 | */
5 | import { getSourceCode } from '../context';
6 |
7 | export const meta = {
8 | docs: {
9 | description: "Prefer toBeEmpty over checking innerHTML",
10 | category: "Best Practices",
11 | recommended: true,
12 | url: "prefer-empty",
13 | },
14 | fixable: "code", // or "code" or "whitespace"
15 | };
16 |
17 | export const create = (context) => {
18 | function isNonEmptyStringOrTemplateLiteral(node) {
19 | return !['""', "''", "``", "null"].includes(
20 | getSourceCode(context).getText(node)
21 | );
22 | }
23 |
24 | return {
25 | [`BinaryExpression[left.property.name='innerHTML'][right.value=''][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
26 | node
27 | ) {
28 | context.report({
29 | node,
30 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
31 | fix: (fixer) => [
32 | fixer.removeRange([node.left.object.range[1], node.range[1]]),
33 | fixer.replaceText(
34 | node.parent.parent.property,
35 | Boolean(node.parent.parent.parent.arguments[0].value) ===
36 | node.operator.startsWith("=") // binary expression XNOR matcher boolean
37 | ? "toBeEmptyDOMElement"
38 | : "not.toBeEmptyDOMElement"
39 | ),
40 | fixer.remove(node.parent.parent.parent.arguments[0]),
41 | ],
42 | });
43 | },
44 | [`BinaryExpression[left.property.name='firstChild'][right.value=null][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
45 | node
46 | ) {
47 | context.report({
48 | node,
49 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
50 | fix: (fixer) => [
51 | fixer.removeRange([node.left.object.range[1], node.range[1]]),
52 | fixer.replaceText(
53 | node.parent.parent.property,
54 | Boolean(node.parent.parent.parent.arguments[0].value) ===
55 | node.operator.startsWith("=") // binary expression XNOR matcher boolean
56 | ? "toBeEmptyDOMElement"
57 | : "not.toBeEmptyDOMElement"
58 | ),
59 | fixer.remove(node.parent.parent.parent.arguments[0]),
60 | ],
61 | });
62 | },
63 | [`MemberExpression[property.name = 'innerHTML'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`](
64 | node
65 | ) {
66 | const args = node.parent.parent.parent.arguments[0];
67 |
68 | if (isNonEmptyStringOrTemplateLiteral(args)) {
69 | return;
70 | }
71 |
72 | context.report({
73 | node,
74 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
75 | fix: (fixer) => [
76 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
77 | fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
78 | fixer.remove(node.parent.parent.parent.arguments[0]),
79 | ],
80 | });
81 | },
82 |
83 | [`MemberExpression[property.name='innerHTML'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`](
84 | node
85 | ) {
86 | const args = node.parent.parent.parent.parent.arguments[0];
87 | if (isNonEmptyStringOrTemplateLiteral(args)) {
88 | return;
89 | }
90 |
91 | context.report({
92 | node,
93 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
94 | fix: (fixer) => [
95 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
96 | fixer.replaceText(
97 | node.parent.parent.parent.property,
98 | "toBeEmptyDOMElement"
99 | ),
100 | fixer.remove(node.parent.parent.parent.parent.arguments[0]),
101 | ],
102 | });
103 | },
104 | [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBeNull$/]`](
105 | node
106 | ) {
107 | context.report({
108 | node,
109 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
110 | fix: (fixer) => [
111 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
112 | fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
113 | ],
114 | });
115 | },
116 | [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`](
117 | node
118 | ) {
119 | if (node.parent.parent.parent.parent.arguments[0].value !== null) {
120 | return;
121 | }
122 |
123 | context.report({
124 | node,
125 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
126 | fix: (fixer) => [
127 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
128 | fixer.replaceText(
129 | node.parent.parent.parent.property,
130 | "toBeEmptyDOMElement"
131 | ),
132 | fixer.remove(node.parent.parent.parent.parent.arguments[0]),
133 | ],
134 | });
135 | },
136 | [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBeNull$/][parent.parent.object.callee.name='expect']`](
137 | node
138 | ) {
139 | context.report({
140 | node,
141 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
142 | fix: (fixer) => [
143 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
144 | fixer.replaceText(
145 | node.parent.parent.parent.property,
146 | "toBeEmptyDOMElement"
147 | ),
148 | ],
149 | });
150 | },
151 | [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`](
152 | node
153 | ) {
154 | if (node.parent.parent.parent.arguments[0].value !== null) {
155 | return;
156 | }
157 |
158 | context.report({
159 | node,
160 | message: "Use toBeEmptyDOMElement instead of checking inner html.",
161 | fix: (fixer) => [
162 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
163 | fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
164 | fixer.remove(node.parent.parent.parent.arguments[0]),
165 | ],
166 | });
167 | },
168 | };
169 | };
170 |
--------------------------------------------------------------------------------
/src/rules/prefer-enabled-disabled.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
3 | * @author Ben Monro
4 | */
5 |
6 | import createBannedAttributeRule from "../createBannedAttributeRule";
7 |
8 | export const meta = {
9 | docs: {
10 | description: "prefer toBeDisabled or toBeEnabled over checking attributes",
11 | category: "Best Practices",
12 | recommended: true,
13 | url: "prefer-enabled-disabled",
14 | },
15 | fixable: "code",
16 | };
17 |
18 | export const create = createBannedAttributeRule({
19 | preferred: "toBeDisabled",
20 | negatedPreferred: "toBeEnabled",
21 | attributes: ["disabled"],
22 | });
23 |
--------------------------------------------------------------------------------
/src/rules/prefer-focus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveFocus over checking activeElementa
3 | * @author Ben Monro
4 | */
5 |
6 | const variantsOfDoc = [
7 | // document:
8 | `[object.name=document]`,
9 | // window.document || global.document:
10 | `[object.object.name=/(global|window)$/][object.property.name=document]`,
11 | // global.window.document:
12 | `[object.object.object.name='global'][object.object.property.name='window'][object.property.name=document]`,
13 | ];
14 |
15 | export const meta = {
16 | docs: {
17 | url: "prefer-focus",
18 | description: "prefer toHaveFocus over checking document.activeElement",
19 | category: "Best Practices",
20 | recommended: true,
21 | },
22 | fixable: "code",
23 | };
24 |
25 | export const create = (context) => ({
26 | [variantsOfDoc
27 | .map(
28 | (variant) =>
29 | `MemberExpression${variant}[property.name='activeElement'][parent.parent.object.callee.name='expect'][parent.parent.property.name='not'][parent.parent.parent.property.name=/to(Be|(Strict)?Equal)$/]`
30 | )
31 | .join(", ")](node) {
32 | const element = node.parent.parent.parent.parent.callee.parent.arguments[0];
33 | const matcher = node.parent.parent.parent.parent.callee.property;
34 |
35 | context.report({
36 | node: node.parent,
37 | message: `Use toHaveFocus instead of checking activeElement`,
38 | fix: (fixer) => {
39 | if (element.name) {
40 | return [
41 | fixer.replaceText(node, element.name),
42 | fixer.remove(element),
43 | fixer.replaceText(matcher, "toHaveFocus"),
44 | ];
45 | }
46 |
47 | return [
48 | fixer.removeRange([node.range[0], element.range[0]]),
49 | fixer.insertTextAfterRange(
50 | [element.range[1], element.range[1] + 1],
51 | ".not.toHaveFocus()"
52 | ),
53 | ];
54 | },
55 | });
56 | },
57 | [variantsOfDoc
58 | .map(
59 | (variant) =>
60 | `MemberExpression${variant}[property.name='activeElement'][parent.callee.object.object.callee.name='expect'][parent.callee.property.name=/to(Be|(Strict)?Equal)$/]`
61 | )
62 | .join(", ")](node) {
63 | const matcher = node.parent.callee.property;
64 | context.report({
65 | node: node.parent,
66 | message: `Use toHaveFocus instead of checking activeElement`,
67 | fix: (fixer) => [
68 | fixer.remove(node),
69 | fixer.replaceText(matcher, "toHaveFocus"),
70 | ],
71 | });
72 | },
73 | [variantsOfDoc
74 | .map(
75 | (variant) =>
76 | `MemberExpression${variant}[property.name='activeElement'][parent.callee.name='expect'][parent.parent.property.name=/to(Be|(Strict)?Equal)$/]`
77 | )
78 | .join(", ")](node) {
79 | const element = node.parent.parent.parent.arguments[0];
80 | const matcher = node.parent.parent.property;
81 | context.report({
82 | node: node.parent,
83 | message: `Use toHaveFocus instead of checking activeElement`,
84 | fix: (fixer) => {
85 | if (!element.name) {
86 | return [
87 | fixer.removeRange([node.range[0], element.range[0]]),
88 | fixer.insertTextAfterRange(
89 | [element.range[1], element.range[1] + 1],
90 | ".toHaveFocus()"
91 | ),
92 | ];
93 | }
94 |
95 | return [
96 | fixer.replaceText(node, element.name),
97 | fixer.remove(element),
98 | fixer.replaceText(matcher, "toHaveFocus"),
99 | ];
100 | },
101 | });
102 | },
103 | [variantsOfDoc
104 | .map(
105 | (variant) =>
106 | `MemberExpression${variant}[property.name='activeElement'][parent.callee.object.callee.name='expect'][parent.callee.property.name=/to(Be|(Strict)?Equal)$/]`
107 | )
108 | .join(", ")](node) {
109 | const matcher = node.parent.callee.property;
110 | context.report({
111 | node: node.parent,
112 | message: `Use toHaveFocus instead of checking activeElement`,
113 | fix: (fixer) => [
114 | fixer.remove(node),
115 | fixer.replaceText(matcher, "toHaveFocus"),
116 | ],
117 | });
118 | },
119 | });
120 |
--------------------------------------------------------------------------------
/src/rules/prefer-in-document.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toBeInTheDocument over checking getAttribute/hasAttribute
3 | * @author Anton Niklasson
4 | */
5 |
6 | /*eslint complexity: ["error", {"max": 20}]*/
7 |
8 | import { queries } from "../queries";
9 | import { getAssignmentForIdentifier } from "../assignment-ast";
10 | import { getSourceCode } from '../context';
11 |
12 | export const meta = {
13 | type: "suggestion",
14 | docs: {
15 | category: "Best Practices",
16 | description:
17 | "Prefer .toBeInTheDocument() for asserting the existence of a DOM node",
18 | url: "prefer-in-document",
19 | recommended: true,
20 | },
21 | fixable: "code",
22 | messages: {
23 | "use-document": `Prefer .toBeInTheDocument() for asserting DOM node existence`,
24 | "invalid-combination-length-1": `Invalid combination of {{ query }} and .toHaveLength(1). Did you mean to use {{ allQuery }}?`,
25 | "replace-query-with-all": `Replace {{ query }} with {{ allQuery }}`,
26 | },
27 | hasSuggestions: true,
28 | };
29 |
30 | function isAntonymMatcher(matcherNode, matcherArguments) {
31 | return (
32 | matcherNode.name === "toBeNull" ||
33 | matcherNode.name === "toBeFalsy" ||
34 | usesToBeOrToEqualWithNull(matcherNode, matcherArguments) ||
35 | usesToHaveLengthZero(matcherNode, matcherArguments)
36 | );
37 | }
38 |
39 | function usesToBeOrToEqualWithNull(matcherNode, matcherArguments) {
40 | return (
41 | (matcherNode.name === "toBe" || matcherNode.name === "toEqual") &&
42 | matcherArguments[0].value === null
43 | );
44 | }
45 |
46 | function usesToHaveLengthZero(matcherNode, matcherArguments) {
47 | // matcherArguments.length === 0: toHaveLength() will cause jest matcher error
48 | // matcherArguments[0].value: toHaveLength(0, ...) means zero length
49 | return (
50 | matcherNode.name === "toHaveLength" &&
51 | (matcherArguments.length === 0 || matcherArguments[0].value === 0)
52 | );
53 | }
54 |
55 | /**
56 | * Extract the DTL query identifier from a call expression
57 | *
58 | * () ->
59 | * screen.() ->
60 | */
61 | function getDTLQueryIdentifierNode(callExpressionNode) {
62 | if (!callExpressionNode || callExpressionNode.type !== "CallExpression") {
63 | return null;
64 | }
65 |
66 | if (callExpressionNode.callee.type === "Identifier") {
67 | return callExpressionNode.callee;
68 | }
69 |
70 | return callExpressionNode.callee.property;
71 | }
72 |
73 | export const create = (context) => {
74 | const alternativeMatchers =
75 | /^(toHaveLength|toBeDefined|toBeNull|toBe|toEqual|toBeTruthy|toBeFalsy)$/;
76 | function getLengthValue(matcherArguments) {
77 | let lengthValue;
78 |
79 | if (matcherArguments[0].type === "Identifier") {
80 | const assignment = getAssignmentForIdentifier(
81 | context,
82 | matcherArguments[0],
83 | matcherArguments[0].name
84 | );
85 | if (!assignment) {
86 | return;
87 | }
88 | lengthValue = assignment.value;
89 | } else if (matcherArguments[0].type === "Literal") {
90 | lengthValue = matcherArguments[0].value;
91 | }
92 |
93 | return lengthValue;
94 | }
95 | function check({
96 | queryNode,
97 | matcherNode,
98 | matcherArguments,
99 | negatedMatcher,
100 | expect,
101 | }) {
102 | if (matcherNode.parent.parent.type !== "CallExpression") {
103 | return;
104 | }
105 |
106 | // only report on dom nodes which we can resolve to RTL queries.
107 | if (!queryNode || (!queryNode.name && !queryNode.property)) return;
108 |
109 | // *By* query with .toHaveLength(0/1) matcher are considered violations
110 | //
111 | // | Selector type | .toHaveLength(1) | .toHaveLength(0) |
112 | // | ============= | =========================== | ===================================== |
113 | // | *By* query | Did you mean to use *AllBy* | Replace with .not.toBeInTheDocument() |
114 | // | *AllBy* query | Correct | Correct
115 | //
116 | // @see https://github.com/testing-library/eslint-plugin-jest-dom/issues/171
117 | //
118 | if (matcherNode.name === "toHaveLength" && matcherArguments.length === 1) {
119 | const lengthValue = getLengthValue(matcherArguments);
120 | const queryName = queryNode.name || queryNode.property.name;
121 |
122 | const isSingleQuery =
123 | queries.includes(queryName) && !/AllBy/.test(queryName);
124 | const hasViolation = isSingleQuery && [1, 0].includes(lengthValue);
125 |
126 | if (!hasViolation) {
127 | return;
128 | }
129 | // If length === 1, report violation with suggestions
130 | // Otherwise fallback to default report
131 | if (lengthValue === 1) {
132 | const allQuery = queryName.replace("By", "AllBy");
133 | return context.report({
134 | node: matcherNode,
135 | messageId: "invalid-combination-length-1",
136 | data: {
137 | query: queryName,
138 | allQuery,
139 | },
140 | loc: matcherNode.loc,
141 | suggest: [
142 | {
143 | messageId: "replace-query-with-all",
144 | data: { query: queryName, allQuery },
145 | fix(fixer) {
146 | return fixer.replaceText(
147 | queryNode.property || queryNode,
148 | allQuery
149 | );
150 | },
151 | },
152 | {
153 | desc: "Replace .toHaveLength(1) with .toBeInTheDocument()",
154 | fix(fixer) {
155 | // Remove any arguments in the matcher
156 | return [
157 | ...Array.from(matcherArguments).map((argument) =>
158 | fixer.remove(argument)
159 | ),
160 | fixer.replaceText(matcherNode, "toBeInTheDocument"),
161 | ];
162 | },
163 | },
164 | ],
165 | });
166 | }
167 | }
168 |
169 | // toBe() or toEqual() are only invalid with null
170 | if (matcherNode.name === "toBe" || matcherNode.name === "toEqual") {
171 | if (
172 | !matcherArguments.length ||
173 | !usesToBeOrToEqualWithNull(matcherNode, matcherArguments)
174 | ) {
175 | return;
176 | }
177 | }
178 |
179 | const query = queryNode.name || queryNode.property.name;
180 |
181 | if (queries.includes(query)) {
182 | context.report({
183 | node: matcherNode,
184 | messageId: "use-document",
185 | loc: matcherNode.loc,
186 | fix(fixer) {
187 | const operations = [];
188 |
189 | // Remove any arguments in the matcher
190 | for (const argument of Array.from(matcherArguments)) {
191 | const sourceCode = getSourceCode(context);
192 | const token = sourceCode.getTokenAfter(argument);
193 | if (token.value === "," && token.type === "Punctuator") {
194 | // Remove commas if toHaveLength had more than one argument or a trailing comma
195 | operations.push(fixer.replaceText(token, ""));
196 | }
197 | operations.push(fixer.remove(argument));
198 | }
199 |
200 | // AllBy should not be used with toBeInTheDocument
201 | operations.push(
202 | fixer.replaceText(
203 | queryNode.property || queryNode,
204 | query.replace("All", "")
205 | )
206 | );
207 | // Flip the .not if necessary
208 | if (isAntonymMatcher(matcherNode, matcherArguments)) {
209 | if (negatedMatcher) {
210 | operations.push(
211 | fixer.replaceTextRange(
212 | [expect.range[1], matcherNode.range[1]],
213 | ".toBeInTheDocument"
214 | )
215 | );
216 |
217 | return operations;
218 | } else {
219 | operations.push(fixer.insertTextBefore(matcherNode, "not."));
220 | }
221 | }
222 |
223 | // Replace the actual matcher
224 | operations.push(fixer.replaceText(matcherNode, "toBeInTheDocument"));
225 |
226 | return operations;
227 | },
228 | });
229 | }
230 | }
231 |
232 | return {
233 | // expect().not.
234 | [`CallExpression[callee.object.object.callee.name='expect'][callee.object.property.name='not'][callee.property.name=${alternativeMatchers}], CallExpression[callee.object.callee.name='expect'][callee.object.property.name='not'][callee.object.arguments.0.argument.callee.name=${alternativeMatchers}]`](
235 | node
236 | ) {
237 | if (!node.callee.object.object.arguments.length) {
238 | return;
239 | }
240 |
241 | const arg = node.callee.object.object.arguments[0];
242 | const queryNode =
243 | arg.type === "AwaitExpression" ? arg.argument.callee : arg.callee;
244 | const matcherNode = node.callee.property;
245 | const matcherArguments = node.arguments;
246 |
247 | const expect = node.callee.object.object;
248 | check({
249 | negatedMatcher: true,
250 | queryNode,
251 | matcherNode,
252 | matcherArguments,
253 | expect,
254 | });
255 | },
256 | // // const foo = expect(foo).not.
257 | [`MemberExpression[object.object.callee.name=expect][object.property.name=not][property.name=${alternativeMatchers}][object.object.arguments.0.type=Identifier]`](
258 | node
259 | ) {
260 | const queryNode = getAssignmentForIdentifier(
261 | context,
262 | node,
263 | node.object.object.arguments[0].name
264 | );
265 |
266 | // Not an RTL query
267 | if (!queryNode || queryNode.type !== "CallExpression") {
268 | return;
269 | }
270 |
271 | const matcherNode = node.property;
272 |
273 | const matcherArguments = node.parent.arguments;
274 |
275 | const expect = node.object.object;
276 | check({
277 | negatedMatcher: true,
278 | queryNode: queryNode.callee,
279 | matcherNode,
280 | matcherArguments,
281 | expect,
282 | });
283 | },
284 | // const foo = expect(foo).
285 | [`MemberExpression[object.callee.name=expect][property.name=${alternativeMatchers}][object.arguments.0.type=Identifier]`](
286 | node
287 | ) {
288 | // Value expression being assigned to the left-hand value
289 | const rightValueNode = getAssignmentForIdentifier(
290 | context,
291 | node,
292 | node.object.arguments[0].name
293 | );
294 |
295 | // Not a DTL query
296 | if (!rightValueNode || rightValueNode.type !== "CallExpression") {
297 | return;
298 | }
299 |
300 | const queryIdentifierNode = getDTLQueryIdentifierNode(rightValueNode);
301 |
302 | const matcherNode = node.property;
303 |
304 | const matcherArguments = node.parent.arguments;
305 | check({
306 | negatedMatcher: false,
307 | queryNode: queryIdentifierNode,
308 | matcherNode,
309 | matcherArguments,
310 | });
311 | },
312 | // expect(await ).
313 | // expect().
314 | [`CallExpression[callee.object.callee.name='expect'][callee.property.name=${alternativeMatchers}], CallExpression[callee.object.callee.name='expect'][callee.object.arguments.0.argument.callee.name=${alternativeMatchers}]`](
315 | node
316 | ) {
317 | const arg = node.callee.object.arguments[0];
318 |
319 | if (!arg) {
320 | return;
321 | }
322 |
323 | const queryIdentifierNode =
324 | arg.type === "AwaitExpression"
325 | ? getDTLQueryIdentifierNode(arg.argument)
326 | : getDTLQueryIdentifierNode(arg);
327 |
328 | const matcherNode = node.callee.property;
329 | const matcherArguments = node.arguments;
330 |
331 | check({
332 | negatedMatcher: false,
333 | queryNode: queryIdentifierNode,
334 | matcherNode,
335 | matcherArguments,
336 | });
337 | },
338 | };
339 | };
340 |
--------------------------------------------------------------------------------
/src/rules/prefer-required.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
3 | * @author Ben Monro
4 | */
5 |
6 | import createBannedAttributeRule from "../createBannedAttributeRule";
7 |
8 | export const meta = {
9 | docs: {
10 | description: "prefer toBeRequired over checking properties",
11 | category: "Best Practices",
12 | recommended: true,
13 | url: "prefer-required",
14 | },
15 | fixable: "code",
16 | };
17 |
18 | export const create = createBannedAttributeRule({
19 | preferred: "toBeRequired",
20 | negatedPreferred: "not.toBeRequired",
21 | attributes: ["required", "aria-required"],
22 | });
23 |
--------------------------------------------------------------------------------
/src/rules/prefer-to-have-attribute.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
3 | * @author Ben Monro
4 | */
5 | import { getSourceCode } from '../context';
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 |
11 | export const meta = {
12 | docs: {
13 | category: "Best Practices",
14 | description:
15 | "prefer toHaveAttribute over checking getAttribute/hasAttribute ",
16 | url: "prefer-to-have-attribute",
17 | recommended: true,
18 | },
19 | fixable: "code",
20 | };
21 |
22 | export const create = (context) => ({
23 | [`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeNull/]`](
24 | node
25 | ) {
26 | context.report({
27 | node: node.parent,
28 | message: `Use toHaveAttribute instead of asserting on getAttribute`,
29 | fix: (fixer) => [
30 | fixer.removeRange([node.callee.object.range[1], node.range[1]]),
31 | fixer.replaceTextRange(
32 | [
33 | node.parent.parent.property.range[0],
34 | node.parent.parent.parent.range[1],
35 | ],
36 | `not.toHaveAttribute(${context
37 | .getSourceCode()
38 | .getText(node.arguments[0])})`
39 | ),
40 | ],
41 | });
42 | },
43 | [`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toContain$|toMatch$/]`](
44 | node
45 | ) {
46 | const sourceCode = getSourceCode(context);
47 | context.report({
48 | node: node.parent,
49 | message: `Use toHaveAttribute instead of asserting on getAttribute`,
50 | fix: (fixer) => [
51 | fixer.removeRange([node.callee.object.range[1], node.range[1]]),
52 | fixer.replaceText(node.parent.parent.property, "toHaveAttribute"),
53 | fixer.replaceText(
54 | node.parent.parent.parent.arguments[0],
55 | `${sourceCode.getText(
56 | node.arguments[0]
57 | )}, expect.string${node.parent.parent.property.name.slice(
58 | 2
59 | )}ing(${sourceCode.getText(node.parent.parent.parent.arguments[0])})`
60 | ),
61 | ],
62 | });
63 | },
64 | [`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
65 | node
66 | ) {
67 | const arg = node.parent.parent.parent.arguments;
68 | const isNull = arg.length > 0 && arg[0].value === null;
69 |
70 | const sourceCode = getSourceCode(context);
71 | context.report({
72 | node: node.parent,
73 | message: `Use toHaveAttribute instead of asserting on getAttribute`,
74 | fix: (fixer) => {
75 | const lastFixer = isNull
76 | ? fixer.replaceText(
77 | node.parent.parent.parent.arguments[0],
78 | sourceCode.getText(node.arguments[0])
79 | )
80 | : fixer.insertTextBefore(
81 | node.parent.parent.parent.arguments[0],
82 | `${sourceCode.getText(node.arguments[0])}, `
83 | );
84 |
85 | return [
86 | fixer.removeRange([node.callee.object.range[1], node.range[1]]),
87 | fixer.replaceText(
88 | node.parent.parent.property,
89 | `${isNull ? "not." : ""}toHaveAttribute`
90 | ),
91 | lastFixer,
92 | ];
93 | },
94 | });
95 | },
96 | [`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeNull|toBeUndefined|toBeDefined/]`](
97 | node
98 | ) {
99 | context.report({
100 | node: node.parent.parent.property,
101 | message: "Invalid matcher for hasAttribute",
102 | });
103 | },
104 | [`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeUndefined|toBeDefined/]`](
105 | node
106 | ) {
107 | context.report({
108 | node: node.parent.parent.property,
109 | message: "Invalid matcher for getAttribute",
110 | });
111 | },
112 | [`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
113 | node
114 | ) {
115 | if (typeof node.parent.parent.parent.arguments[0].value === "boolean") {
116 | context.report({
117 | node: node.parent,
118 | message: `Use toHaveAttribute instead of asserting on hasAttribute`,
119 | fix: (fixer) => [
120 | fixer.removeRange([node.callee.object.range[1], node.range[1]]),
121 | fixer.replaceText(
122 | node.parent.parent.property,
123 | `${
124 | node.parent.parent.parent.arguments[0].value === false
125 | ? "not."
126 | : ""
127 | }toHaveAttribute`
128 | ),
129 | fixer.replaceText(
130 | node.parent.parent.parent.arguments[0],
131 | getSourceCode(context).getText(node.arguments[0])
132 | ),
133 | ],
134 | });
135 | } else {
136 | context.report({
137 | node: node.parent.parent.property,
138 | message: "Invalid matcher for hasAttribute",
139 | });
140 | }
141 | },
142 | [`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeTruthy|toBeFalsy/]`](
143 | node
144 | ) {
145 | context.report({
146 | node: node.parent,
147 | message: `Use toHaveAttribute instead of asserting on hasAttribute`,
148 | fix: (fixer) => [
149 | fixer.removeRange([node.callee.object.range[1], node.range[1]]),
150 | fixer.replaceTextRange(
151 | [
152 | node.parent.parent.property.range[0],
153 | node.parent.parent.parent.range[1],
154 | ],
155 | `${
156 | node.parent.parent.property.name === "toBeFalsy" ? "not." : ""
157 | }toHaveAttribute(${context
158 | .getSourceCode()
159 | .getText(node.arguments[0])})`
160 | ),
161 | ],
162 | });
163 | },
164 | });
165 |
--------------------------------------------------------------------------------
/src/rules/prefer-to-have-class.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveClass over checking element className
3 | * @author Ben Monro
4 | */
5 |
6 | import { getQueryNodeFrom } from "../assignment-ast";
7 | import { getSourceCode } from '../context';
8 |
9 | //------------------------------------------------------------------------------
10 | // Rule Definition
11 | //------------------------------------------------------------------------------
12 |
13 | const messageId = "use-to-have-class";
14 | export const meta = {
15 | docs: {
16 | category: "Best Practices",
17 | url: "prefer-to-have-class",
18 | description: "prefer toHaveClass over checking element className",
19 | recommended: true,
20 | },
21 | messages: {
22 | [messageId]: `Prefer .toHaveClass() over checking element className`,
23 | },
24 | fixable: "code",
25 | };
26 |
27 | export const create = (context) => ({
28 | //expect(el.classList.contains("foo")).toBe(true)
29 | [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.callee.object.property.name=classList][callee.object.arguments.0.callee.property.name=contains][callee.property.name=/toBe(Truthy|Falsy)?|to(Strict)?Equal/]`](
30 | node
31 | ) {
32 | const classValue = node.callee.object.arguments[0].arguments[0];
33 | const checkedProp = node.callee.object.arguments[0].callee.object.object;
34 | const matcher = node.callee.property;
35 | const [matcherArg] = node.arguments;
36 | const [expectArg] = node.callee.object.arguments;
37 | const isTruthy =
38 | (matcher.name === "toBe" && matcherArg.value === true) ||
39 | matcher.name === "toBeTruthy";
40 |
41 | context.report({
42 | node: matcher,
43 | messageId,
44 | fix(fixer) {
45 | return [
46 | fixer.removeRange([checkedProp.range[1], expectArg.range[1]]),
47 |
48 | fixer.replaceText(matcher, `${isTruthy ? "" : "not."}toHaveClass`),
49 | matcherArg
50 | ? fixer.replaceText(
51 | matcherArg,
52 | getSourceCode(context).getText(classValue)
53 | )
54 | : fixer.insertTextBefore(
55 | getSourceCode(context).getTokenAfter(matcher, { skip: 1 }),
56 | getSourceCode(context).getText(classValue)
57 | ),
58 | ];
59 | },
60 | });
61 | },
62 |
63 | //expect(el.classList[0]).toBe("bar")
64 | [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.object.property.name=classList][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`](
65 | node
66 | ) {
67 | const [classValue] = node.arguments;
68 | const matcher = node.callee.property;
69 | const classNameProp = node.callee.object.arguments[0].object;
70 | const expectArg = node.callee.object.arguments[0];
71 |
72 | context.report({
73 | node: matcher,
74 | messageId,
75 | fix(fixer) {
76 | //can't autofix here as it toHaveClass doesn't have a partial matcher / regex for class names.
77 | if (matcher.name === "toContain") return;
78 | return [
79 | fixer.removeRange([
80 | classNameProp.object.range[1],
81 | expectArg.range[1],
82 | ]),
83 | fixer.replaceText(matcher, "toHaveClass"),
84 | fixer.replaceText(
85 | classValue,
86 | getSourceCode(context).getText(classValue)
87 | ),
88 | ];
89 | },
90 | });
91 | },
92 |
93 | //expect(el.classList[0]).not.toBe("bar")
94 | [`CallExpression[callee.object.object.callee.name=expect][callee.object.object.arguments.0.object.property.name=classList][callee.object.property.name=not][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`](
95 | node
96 | ) {
97 | //can't autofix this case because the class could be in another element of the classList array.
98 | context.report({
99 | node,
100 | messageId,
101 | });
102 | },
103 | //expect(el.className | el.classList).toBe("bar") / toStrict?Equal / toContain
104 | [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=/class(Name|List)/][callee.property.name=/toBe$|to(Strict)?Equal|toContain/]`](
105 | node
106 | ) {
107 | const checkedProp = node.callee.object.arguments[0].property;
108 | const [classValue] = node.arguments;
109 | const matcher = node.callee.property;
110 | const classNameProp = node.callee.object.arguments[0].object;
111 |
112 | const { isDTLQuery } = getQueryNodeFrom(context, classNameProp);
113 | if (!isDTLQuery) return;
114 | // don't report here if using `expect.foo()`
115 |
116 | if (
117 | classValue.type === "CallExpression" &&
118 | classValue.callee.type === "MemberExpression" &&
119 | classValue.callee.object.name === "expect"
120 | ) {
121 | return;
122 | }
123 |
124 | context.report({
125 | node: matcher,
126 | messageId,
127 | fix(fixer) {
128 | if (checkedProp.name === "classList" && matcher.name !== "toContain") {
129 | return;
130 | }
131 |
132 | return [
133 | fixer.removeRange([classNameProp.range[1], checkedProp.range[1]]),
134 | fixer.replaceText(matcher, "toHaveClass"),
135 | fixer.replaceText(
136 | classValue,
137 | `${getSourceCode(context).getText(classValue)}${
138 | matcher.name === "toContain" ? "" : ", { exact: true }"
139 | }`
140 | ),
141 | ];
142 | },
143 | });
144 | },
145 |
146 | //expect(el.className | el.classList).toEqual(expect.stringContaining("foo") | objectContaining) / toStrictEqual
147 | [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=/class(Name|List)/][callee.property.name=/to(Strict)?Equal/][arguments.0.callee.object.name=expect]`](
148 | node
149 | ) {
150 | const className = node.callee.object.arguments[0].property;
151 | const [classValue] = node.arguments[0].arguments;
152 | const matcher = node.callee.property;
153 | const classNameProp = node.callee.object.arguments[0].object;
154 | const matcherArg = node.arguments[0].callee.property;
155 | const { isDTLQuery } = getQueryNodeFrom(context, classNameProp);
156 | if (!isDTLQuery) return;
157 |
158 | context.report({
159 | node: matcher,
160 | messageId,
161 | fix(fixer) {
162 | if (matcherArg.name !== "stringContaining") return;
163 | return [
164 | fixer.removeRange([classNameProp.range[1], className.range[1]]),
165 | fixer.replaceText(matcher, "toHaveClass"),
166 | fixer.replaceText(
167 | node.arguments[0],
168 | `${getSourceCode(context).getText(classValue)}`
169 | ),
170 | ];
171 | },
172 | });
173 | },
174 |
175 | //expect(screen.getByRole("button").className | classList).not.toBe("foo"); / toStrict?Equal / toContain
176 | [`CallExpression[callee.object.object.callee.name=expect][callee.object.object.arguments.0.property.name=/class(Name|List)/][callee.object.property.name=not][callee.property.name=/toBe$|to(Strict)?Equal|toContain/]`](
177 | node
178 | ) {
179 | const className = node.callee.object.object.arguments[0].property;
180 | const [classValue] = node.arguments;
181 | const matcher = node.callee.property;
182 | const classNameProp = node.callee.object.object.arguments[0].object;
183 |
184 | const { isDTLQuery } = getQueryNodeFrom(context, classNameProp);
185 | if (!isDTLQuery) return;
186 | context.report({
187 | node: matcher,
188 | messageId,
189 | fix(fixer) {
190 | if (className.name === "classList" && matcher.name !== "toContain") {
191 | return;
192 | }
193 |
194 | return [
195 | fixer.removeRange([classNameProp.range[1], className.range[1]]),
196 | fixer.replaceText(matcher, "toHaveClass"),
197 | fixer.replaceText(
198 | classValue,
199 | `${getSourceCode(context).getText(classValue)}${
200 | matcher.name === "toContain" ? "" : ", { exact: true }"
201 | }`
202 | ),
203 | ];
204 | },
205 | });
206 | },
207 |
208 | //expect(el).toHaveProperty("className", "foo: bar");
209 | //expect(el).toHaveAttribute("class", "foo: bar");
210 | [[
211 | `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`,
212 | `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`,
213 | ].join(",")](node) {
214 | const matcher = node.callee.property;
215 | const [classArg, classValueArg] = node.arguments;
216 |
217 | const classNameValue = context
218 | .getSourceCode()
219 | .getText(classArg)
220 | .slice(1, -1);
221 | if (
222 | (matcher.name === "toHaveAttribute" && classNameValue !== "class") ||
223 | (matcher.name === "toHaveProperty" && classNameValue !== "className")
224 | ) {
225 | return;
226 | }
227 |
228 | const { isDTLQuery } = getQueryNodeFrom(
229 | context,
230 | node.callee.object.arguments[0]
231 | );
232 | if (!isDTLQuery) return;
233 | context.report({
234 | node: matcher,
235 | messageId,
236 | fix(fixer) {
237 | return [
238 | fixer.replaceText(matcher, "toHaveClass"),
239 | fixer.replaceText(
240 | classArg,
241 | getSourceCode(context).getText(classValueArg)
242 | ),
243 | fixer.replaceText(classValueArg, `{ exact: true }`),
244 | ];
245 | },
246 | });
247 | },
248 |
249 | //expect(el).not.toHaveAttribute("class", "foo: bar");
250 | //expect(el).not.toHaveProperty("className", "foo: bar");
251 | [[
252 | `CallExpression[callee.object.object.callee.name=expect][callee.object.property.name=not][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`,
253 | `CallExpression[callee.object.object.callee.name=expect][callee.object.property.name=not][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`,
254 | ].join(",")](node) {
255 | //[callee.object.property.name=/toHaveAttribute|toHaveProperty/][arguments.0.value=class][arguments.1.type=/Literal$/]
256 | const matcher = node.callee.property;
257 | const [classArg, classValueArg] = node.arguments;
258 | const classNameValue = context
259 | .getSourceCode()
260 | .getText(classArg)
261 | .slice(1, -1);
262 | if (
263 | (matcher.name === "toHaveAttribute" && classNameValue !== "class") ||
264 | (matcher.name === "toHaveProperty" && classNameValue !== "className")
265 | ) {
266 | return;
267 | }
268 |
269 | const { isDTLQuery } = getQueryNodeFrom(
270 | context,
271 | node.callee.object.object.arguments[0]
272 | );
273 | if (!isDTLQuery) return;
274 | context.report({
275 | node: matcher,
276 | messageId,
277 | fix(fixer) {
278 | return [
279 | fixer.replaceText(matcher, "toHaveClass"),
280 | fixer.replaceText(
281 | classArg,
282 | getSourceCode(context).getText(classValueArg)
283 | ),
284 | fixer.replaceText(classValueArg, `{ exact: true }`),
285 | ];
286 | },
287 | });
288 | },
289 |
290 | //expect(el).toHaveProperty(`className`, expect.stringContaining("foo"));
291 | //expect(el).toHaveAttribute(`class`, expect.stringContaining("foo"));
292 | [[
293 | `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.callee.object.name=expect][arguments.1.callee.property.name=stringContaining]`,
294 | `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.callee.object.name=expect][arguments.1.callee.property.name=stringContaining]`,
295 | ].join(",")](node) {
296 | const matcher = node.callee.property;
297 | const [classArg, classValue] = node.arguments;
298 | const classValueArg = classValue.arguments[0];
299 |
300 | const classNameValue = context
301 | .getSourceCode()
302 | .getText(classArg)
303 | .slice(1, -1);
304 | if (
305 | (matcher.name === "toHaveAttribute" && classNameValue !== "class") ||
306 | (matcher.name === "toHaveProperty" && classNameValue !== "className")
307 | ) {
308 | return;
309 | }
310 |
311 | const { isDTLQuery } = getQueryNodeFrom(
312 | context,
313 | node.callee.object.arguments[0]
314 | );
315 | if (!isDTLQuery) return;
316 |
317 | context.report({
318 | node: matcher,
319 | messageId,
320 | fix(fixer) {
321 | return [
322 | fixer.replaceText(matcher, "toHaveClass"),
323 | fixer.replaceText(
324 | classArg,
325 | getSourceCode(context).getText(classValueArg)
326 | ),
327 | fixer.removeRange([classArg.range[1], classValue.range[1]]),
328 | ];
329 | },
330 | });
331 | },
332 | });
333 |
--------------------------------------------------------------------------------
/src/rules/prefer-to-have-style.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveStyle over checking element style
3 | * @author Ben Monro
4 | */
5 | import { getSourceCode } from '../context';
6 |
7 | //------------------------------------------------------------------------------
8 | // Rule Definition
9 | //------------------------------------------------------------------------------
10 | const camelCase = (str) => str.replace(/-([a-z])/g, (c) => c[1].toUpperCase());
11 | export const meta = {
12 | docs: {
13 | category: "Best Practices",
14 | url: "prefer-to-have-style",
15 | description: "prefer toHaveStyle over checking element style",
16 | recommended: true,
17 | },
18 | fixable: "code",
19 | };
20 |
21 | export const create = (context) => {
22 | function getReplacementObjectProperty(styleName) {
23 | if (styleName.type === "Literal") {
24 | return camelCase(styleName.value);
25 | }
26 |
27 | return `[${getSourceCode(context).getText(styleName)}]`;
28 | }
29 | function getReplacementStyleParam(styleName, styleValue) {
30 | return styleName.type === "Literal"
31 | ? `{${camelCase(styleName.value)}: ${context
32 | .getSourceCode()
33 | .getText(styleValue)}}`
34 | : `${getSourceCode(context).getText(styleName).slice(0, -1)}: ${
35 | styleValue.type === "TemplateLiteral"
36 | ? getSourceCode(context).getText(styleValue).substring(1)
37 | : `${styleValue.value}\``
38 | }`;
39 | }
40 |
41 | return {
42 | //expect(el.style.foo).toBe("bar");
43 | [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal/][parent.parent.callee.name=expect]`](
44 | node
45 | ) {
46 | const styleName = node.parent.property;
47 | const [styleValue] = node.parent.parent.parent.parent.arguments;
48 | const matcher = node.parent.parent.parent.property;
49 | context.report({
50 | node: node.property,
51 | message: "Use toHaveStyle instead of asserting on element style",
52 | fix(fixer) {
53 | return [
54 | fixer.removeRange([node.object.range[1], styleName.range[1]]),
55 | fixer.replaceText(matcher, "toHaveStyle"),
56 | fixer.replaceText(
57 | styleValue,
58 | `{${styleName.name}:${context
59 | .getSourceCode()
60 | .getText(styleValue)}}`
61 | ),
62 | ];
63 | },
64 | });
65 | },
66 | //expect(el.style.foo).not.toBe("bar");
67 | [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=not][parent.parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.parent.arguments.0.type=/(Template)?Literal$/][parent.parent.callee.name=expect]`](
68 | node
69 | ) {
70 | const styleName = node.parent.property;
71 | const styleValue = node.parent.parent.parent.parent.parent.arguments[0];
72 | const matcher = node.parent.parent.parent.parent.property;
73 | context.report({
74 | node: node.property,
75 | message: "Use toHaveStyle instead of asserting on element style",
76 | fix(fixer) {
77 | return [
78 | fixer.removeRange([node.object.range[1], styleName.range[1]]),
79 | fixer.replaceText(matcher, "toHaveStyle"),
80 | fixer.replaceText(
81 | styleValue,
82 | `{${styleName.name}:${context
83 | .getSourceCode()
84 | .getText(styleValue)}}`
85 | ),
86 | ];
87 | },
88 | });
89 | },
90 | // expect(el.style).toContain("foo-bar")
91 | [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.parent.parent.arguments.0.type=/(Template)?Literal$/][parent.callee.name=expect]`](
92 | node
93 | ) {
94 | const [styleName] = node.parent.parent.parent.arguments;
95 | const matcher = node.parent.parent.property;
96 |
97 | context.report({
98 | node: node.property,
99 | message: "Use toHaveStyle instead of asserting on element style",
100 | fix(fixer) {
101 | return [
102 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
103 | fixer.replaceText(matcher, "toHaveStyle"),
104 | fixer.replaceText(
105 | styleName,
106 | styleName.type === "Literal"
107 | ? `{${camelCase(styleName.value)}: expect.anything()}`
108 | : getSourceCode(context).getText(styleName)
109 | ),
110 | ];
111 | },
112 | });
113 | },
114 | // expect(el.style).not.toContain("foo-bar")
115 | [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toContain][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal$/]`](
116 | node
117 | ) {
118 | const [styleName] = node.parent.parent.parent.parent.arguments;
119 | const matcher = node.parent.parent.parent.property;
120 |
121 | context.report({
122 | node: node.property,
123 | message: "Use toHaveStyle instead of asserting on element style",
124 | fix(fixer) {
125 | return [
126 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
127 | fixer.replaceText(matcher, "toHaveStyle"),
128 | fixer.replaceText(
129 | styleName,
130 | styleName.type === "Literal"
131 | ? `{${camelCase(styleName.value)}: expect.anything()}`
132 | : getSourceCode(context).getText(styleName)
133 | ),
134 | ];
135 | },
136 | });
137 | },
138 |
139 | //expect(el).toHaveAttribute("style", "foo: bar");
140 | [`CallExpression[callee.property.name=toHaveAttribute][arguments.0.value=style][arguments.1][callee.object.callee.name=expect]`](
141 | node
142 | ) {
143 | context.report({
144 | node: node.arguments[0],
145 | message: "Use toHaveStyle instead of asserting on element style",
146 | fix(fixer) {
147 | return [
148 | fixer.replaceText(node.callee.property, "toHaveStyle"),
149 | fixer.removeRange([
150 | node.arguments[0].range[0],
151 | node.arguments[1].range[0],
152 | ]),
153 | ];
154 | },
155 | });
156 | },
157 |
158 | //expect(el.style["foo-bar"]).toBe("baz")
159 | [`MemberExpression[property.name=style][parent.computed=true][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.arguments.0.type=/((Template)?Literal|Identifier)/][parent.parent.callee.name=expect]`](
160 | node
161 | ) {
162 | const styleName = node.parent.property;
163 | const [styleValue] = node.parent.parent.parent.parent.arguments;
164 | const matcher = node.parent.parent.parent.property;
165 | const startOfStyleMemberExpression = node.object.range[1];
166 | const endOfStyleMemberExpression =
167 | node.parent.parent.arguments[0].range[1];
168 |
169 | let fix = null;
170 |
171 | if (
172 | typeof styleValue.value !== "number" &&
173 | !(styleValue.value instanceof RegExp) &&
174 | styleName.type !== "Identifier"
175 | ) {
176 | fix = (fixer) => {
177 | return [
178 | fixer.removeRange([
179 | startOfStyleMemberExpression,
180 | endOfStyleMemberExpression,
181 | ]),
182 | fixer.replaceText(matcher, "toHaveStyle"),
183 | fixer.replaceText(
184 | styleValue,
185 | typeof styleName.value === "number"
186 | ? `{${getReplacementObjectProperty(
187 | styleValue
188 | )}: expect.anything()}`
189 | : getReplacementStyleParam(styleName, styleValue)
190 | ),
191 | ];
192 | };
193 | }
194 |
195 | context.report({
196 | node: node.property,
197 | message: "Use toHaveStyle instead of asserting on element style",
198 | fix,
199 | });
200 | },
201 | //expect(el.style["foo-bar"]).not.toBe("baz")
202 | [`MemberExpression[property.name=style][parent.computed=true][parent.parent.parent.property.name=not][parent.parent.parent.parent.parent.callee.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.parent.arguments.0.type=/(Template)?Literal/][parent.parent.callee.name=expect]`](
203 | node
204 | ) {
205 | const styleName = node.parent.property;
206 | const [styleValue] = node.parent.parent.parent.parent.parent.arguments;
207 | const matcher = node.parent.parent.parent.parent.property;
208 | const endOfStyleMemberExpression =
209 | node.parent.parent.arguments[0].range[1];
210 |
211 | let fix = null;
212 |
213 | if (
214 | typeof styleName.value !== "number" &&
215 | styleName.type !== "Identifier"
216 | ) {
217 | fix = (fixer) => {
218 | return [
219 | fixer.removeRange([
220 | node.object.range[1],
221 | endOfStyleMemberExpression,
222 | ]),
223 | fixer.replaceText(matcher, "toHaveStyle"),
224 | fixer.replaceText(
225 | styleValue,
226 | getReplacementStyleParam(styleName, styleValue)
227 | ),
228 | ];
229 | };
230 | }
231 |
232 | context.report({
233 | node: node.property,
234 | message: "Use toHaveStyle instead of asserting on element style",
235 | fix,
236 | });
237 | },
238 | //expect(foo.style).toHaveProperty("foo", "bar")
239 | [`MemberExpression[property.name=style][parent.parent.property.name=toHaveProperty][parent.callee.name=expect]`](
240 | node
241 | ) {
242 | const [styleName, styleValue] = node.parent.parent.parent.arguments;
243 | const matcher = node.parent.parent.property;
244 |
245 | context.report({
246 | node: node.property,
247 | message: "Use toHaveStyle instead of asserting on element style",
248 | fix(fixer) {
249 | if (
250 | !styleValue ||
251 | !["Literal", "TemplateLiteral"].includes(styleValue.type)
252 | ) {
253 | return null;
254 | }
255 | return [
256 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
257 | fixer.replaceText(matcher, "toHaveStyle"),
258 | fixer.replaceTextRange(
259 | [styleName.range[0], styleValue.range[1]],
260 | `{${getReplacementObjectProperty(styleName)}: ${context
261 | .getSourceCode()
262 | .getText(styleValue)}}`
263 | ),
264 | ];
265 | },
266 | });
267 | },
268 |
269 | //expect(foo.style).not.toHaveProperty("foo", "bar")
270 | [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toHaveProperty][parent.callee.name=expect]`](
271 | node
272 | ) {
273 | const [styleName, styleValue] =
274 | node.parent.parent.parent.parent.arguments;
275 | const matcher = node.parent.parent.parent.property;
276 |
277 | context.report({
278 | node: node.property,
279 | message: "Use toHaveStyle instead of asserting on element style",
280 | fix(fixer) {
281 | if (
282 | !styleValue ||
283 | !["Literal", "TemplateLiteral"].includes(styleValue.type)
284 | ) {
285 | return null;
286 | }
287 | return [
288 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
289 | fixer.replaceText(matcher, "toHaveStyle"),
290 | fixer.replaceTextRange(
291 | [styleName.range[0], styleValue.range[1]],
292 | `{${getReplacementObjectProperty(styleName)}: ${context
293 | .getSourceCode()
294 | .getText(styleValue)}}`
295 | ),
296 | ];
297 | },
298 | });
299 | },
300 | };
301 | };
302 |
--------------------------------------------------------------------------------
/src/rules/prefer-to-have-text-content.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
3 | * @author Ben Monro
4 | */
5 | import { getSourceCode } from '../context';
6 |
7 | export const meta = {
8 | docs: {
9 | category: "Best Practices",
10 | url: "prefer-to-have-text-content",
11 | description: "Prefer toHaveTextContent over checking element.textContent",
12 | recommended: true,
13 | },
14 | fixable: "code",
15 | };
16 |
17 | export const create = (context) => ({
18 | [`MemberExpression[property.name='textContent'][parent.callee.name='expect'][parent.parent.property.name=/toContain$|toMatch$/]`](
19 | node
20 | ) {
21 | const expectedArg = node.parent.parent.parent.arguments[0];
22 |
23 | const expectedArgSource = getSourceCode(context).getText(expectedArg);
24 | context.report({
25 | node: node.parent,
26 | message: `Use toHaveTextContent instead of asserting on DOM node attributes`,
27 | fix: (fixer) => {
28 | return [
29 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
30 | fixer.replaceTextRange(
31 | node.parent.parent.property.range,
32 | "toHaveTextContent"
33 | ),
34 | fixer.replaceTextRange(
35 | expectedArg.range,
36 | expectedArg.type === "Literal"
37 | ? expectedArg.regex
38 | ? expectedArgSource
39 | : new RegExp(
40 | expectedArg.value
41 | .toString()
42 | .replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&")
43 | )
44 | : `new RegExp(${expectedArgSource})`
45 | ),
46 | ];
47 | },
48 | });
49 | },
50 | [`MemberExpression[property.name='textContent'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
51 | node
52 | ) {
53 | context.report({
54 | node: node.parent,
55 | message: `Use toHaveTextContent instead of asserting on DOM node attributes`,
56 | fix: (fixer) => [
57 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
58 | fixer.replaceTextRange(
59 | node.parent.parent.property.range,
60 | "toHaveTextContent"
61 | ),
62 | ],
63 | });
64 | },
65 | [`MemberExpression[property.name='textContent'][parent.callee.name='expect'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
66 | node
67 | ) {
68 | context.report({
69 | node: node.parent,
70 | message: `Use toHaveTextContent instead of asserting on DOM node attributes`,
71 | fix: (fixer) => [
72 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
73 | fixer.replaceTextRange(
74 | node.parent.parent.parent.property.range,
75 | "toHaveTextContent"
76 | ),
77 | ],
78 | });
79 | },
80 | [`MemberExpression[property.name='textContent'][parent.callee.name='expect'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toContain$|toMatch$/]`](
81 | node
82 | ) {
83 | const expectedArg = node.parent.parent.parent.parent.arguments[0];
84 | const expectedArgSource = getSourceCode(context).getText(expectedArg);
85 | context.report({
86 | node: node.parent,
87 | message: `Use toHaveTextContent instead of asserting on DOM node attributes`,
88 | fix: (fixer) => [
89 | fixer.removeRange([node.object.range[1], node.property.range[1]]),
90 | fixer.replaceTextRange(
91 | node.parent.parent.parent.property.range,
92 | "toHaveTextContent"
93 | ),
94 | fixer.replaceTextRange(
95 | expectedArg.range,
96 | expectedArg.type === "Literal"
97 | ? expectedArg.regex
98 | ? expectedArgSource
99 | : new RegExp(
100 | expectedArg.value
101 | .toString()
102 | .replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&")
103 | )
104 | : `new RegExp(${expectedArgSource})`
105 | ),
106 | ],
107 | });
108 | },
109 | });
110 |
--------------------------------------------------------------------------------
/src/rules/prefer-to-have-value.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
3 | * @author Ben Monro
4 | */
5 |
6 | import { getQueryNodeFrom } from "../assignment-ast";
7 | import { getSourceCode } from '../context';
8 |
9 | //------------------------------------------------------------------------------
10 | // Rule Definition
11 | //------------------------------------------------------------------------------
12 |
13 | export const meta = {
14 | docs: {
15 | category: "Best Practices",
16 | description: "prefer toHaveValue over checking element.value",
17 | url: "prefer-to-have-value",
18 | recommended: true,
19 | },
20 | fixable: "code",
21 | messages: {
22 | "use-to-have-value": `Prefer .toHaveValue() over other attribute checks`,
23 | },
24 | };
25 | const messageId = "use-to-have-value";
26 |
27 | export const create = (context) => {
28 | function isValidQueryNode(nodeWithValueProp) {
29 | const { query, queryArg, isDTLQuery } = getQueryNodeFrom(
30 | context,
31 | nodeWithValueProp
32 | );
33 | return (
34 | !!query &&
35 | isDTLQuery &&
36 | !!query.match(/^(get|find|query)(All)?ByRole$/) &&
37 | ["textbox", "dropdown"].includes(queryArg)
38 | );
39 | }
40 | return {
41 | // expect(element.value).toBe('foo') / toEqual / toStrictEqual
42 | // expect(.value).toBe('foo') / toEqual / toStrictEqual
43 | // expect((await ).value).toBe('foo') / toEqual / toStrictEqual
44 | [`CallExpression[callee.property.name=/to(Be|(Strict)?Equal)$/][callee.object.arguments.0.property.name=value][callee.object.callee.name=expect]`](
45 | node
46 | ) {
47 | const valueProp = node.callee.object.arguments[0].property;
48 | const matcher = node.callee.property;
49 | const queryNode = node.callee.object.arguments[0].object;
50 |
51 | if (isValidQueryNode(queryNode)) {
52 | context.report({
53 | messageId,
54 | node,
55 | fix(fixer) {
56 | return [
57 | fixer.remove(getSourceCode(context).getTokenBefore(valueProp)),
58 | fixer.remove(valueProp),
59 | fixer.replaceText(matcher, "toHaveValue"),
60 | ];
61 | },
62 | });
63 | }
64 | },
65 |
66 | // expect(element.value).not.toBe('foo') / toEqual / toStrictEqual
67 | // expect(.value).not.toBe('foo') / toEqual / toStrictEqual
68 | // expect((await ).value).not.toBe('foo') / toEqual / toStrictEqual
69 | [`CallExpression[callee.property.name=/to(Be|(Strict)?Equal)$/][callee.object.object.callee.name=expect][callee.object.property.name=not][callee.object.object.arguments.0.property.name=value]`](
70 | node
71 | ) {
72 | const queryNode = node.callee.object.object.arguments[0].object;
73 | const valueProp = node.callee.object.object.arguments[0].property;
74 | const matcher = node.callee.property;
75 |
76 | if (isValidQueryNode(queryNode)) {
77 | context.report({
78 | messageId,
79 | node,
80 | fix(fixer) {
81 | return [
82 | fixer.removeRange([
83 | getSourceCode(context).getTokenBefore(valueProp).range[0],
84 | valueProp.range[1],
85 | ]),
86 | fixer.replaceText(matcher, "toHaveValue"),
87 | ];
88 | },
89 | });
90 | }
91 | },
92 |
93 | //expect(element).toHaveAttribute('value', 'foo') / Property
94 | [`CallExpression[callee.property.name=/toHave(Attribute|Property)/][arguments.0.value=value][arguments.1][callee.object.callee.name=expect], CallExpression[callee.property.name=/toHave(Attribute|Property)/][arguments.0.value=value][arguments.1][callee.object.object.callee.name=expect][callee.object.property.name=not]`](
95 | node
96 | ) {
97 | const matcher = node.callee.property;
98 | const [prop, value] = node.arguments;
99 |
100 | context.report({
101 | messageId,
102 | node,
103 |
104 | fix(fixer) {
105 | return [
106 | fixer.replaceText(matcher, "toHaveValue"),
107 | fixer.removeRange([prop.range[0], value.range[0]]),
108 | ];
109 | },
110 | });
111 | },
112 | };
113 | };
114 |
--------------------------------------------------------------------------------