├── .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 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |

Ben Monro

📖 💻 💡 ⚠️

Nick McCurdy

💻 📖 ⚠️

Ernesto García

📖

Chris Colborne

💻 ⚠️

Michaël De Boey

💻

Gerrit Alex

💻 ⚠️ 📖 🐛

Andrey Los

🐛

Spencer Miskoviak

💻 ⚠️

Aleksei Tsikov

🐛

Mario Beltrán Alarcón

📖

Ari Perkkiö

🐛 💻 ⚠️

Anton Niklasson

💻 ⚠️ 📖

Juzer Zarif

💻 ⚠️ 🐛

Julien Wajsberg

💻 ⚠️

Gareth Jones

⚠️ 💻 🐛

Huyen Nguyen

📖

Matthew

🐛 💻
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 | --------------------------------------------------------------------------------