├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Bug_Report.md │ ├── Feature_Request.md │ └── Question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .huskyrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest-globals.d.ts ├── jest-globals.js ├── jest.config.js ├── matchers.d.ts ├── matchers.js ├── other ├── CODE_OF_CONDUCT.md ├── MAINTAINING.md ├── USERS.md ├── manual-releases.md └── owl.png ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── helpers │ │ ├── document.js │ │ └── test-utils.js │ ├── to-be-checked.js │ ├── to-be-disabled.js │ ├── to-be-empty-dom-element.js │ ├── to-be-empty.js │ ├── to-be-in-the-document.js │ ├── to-be-in-the-dom.js │ ├── to-be-invalid.js │ ├── to-be-partially-checked.js │ ├── to-be-required.js │ ├── to-be-visible.js │ ├── to-contain-element.js │ ├── to-contain-html.js │ ├── to-have-accessible-description.js │ ├── to-have-accessible-errormessage.js │ ├── to-have-accessible-name.js │ ├── to-have-attribute.js │ ├── to-have-class.js │ ├── to-have-description.js │ ├── to-have-display-value.js │ ├── to-have-errormessage.js │ ├── to-have-focus.js │ ├── to-have-form-values.js │ ├── to-have-role.js │ ├── to-have-selection.js │ ├── to-have-style.js │ ├── to-have-text-content.js │ ├── to-have-value.js │ └── utils.js ├── index.js ├── jest-globals.js ├── matchers.js ├── to-be-checked.js ├── to-be-disabled.js ├── to-be-empty-dom-element.js ├── to-be-empty.js ├── to-be-in-the-document.js ├── to-be-in-the-dom.js ├── to-be-invalid.js ├── to-be-partially-checked.js ├── to-be-required.js ├── to-be-visible.js ├── to-contain-element.js ├── to-contain-html.js ├── to-have-accessible-description.js ├── to-have-accessible-errormessage.js ├── to-have-accessible-name.js ├── to-have-attribute.js ├── to-have-class.js ├── to-have-description.js ├── to-have-display-value.js ├── to-have-errormessage.js ├── to-have-focus.js ├── to-have-form-values.js ├── to-have-role.js ├── to-have-selection.js ├── to-have-style.js ├── to-have-text-content.js ├── to-have-value.js ├── utils.js └── vitest.js ├── tests ├── jest.config.dom.js ├── jest.config.node.js └── setup-env.js ├── tsconfig.json ├── types ├── __tests__ │ ├── bun │ │ ├── bun-custom-expect-types.test.ts │ │ ├── bun-types.test.ts │ │ └── tsconfig.json │ ├── jest-globals │ │ ├── jest-globals-custom-expect-types.test.ts │ │ ├── jest-globals-types.test.ts │ │ └── tsconfig.json │ ├── jest │ │ ├── jest-custom-expect-types.test.ts │ │ ├── jest-types.test.ts │ │ └── tsconfig.json │ └── vitest │ │ ├── tsconfig.json │ │ ├── vitest-custom-expect-types.test.ts │ │ └── vitest-types.test.ts ├── bun.d.ts ├── index.d.ts ├── jest-globals.d.ts ├── jest.d.ts ├── matchers-standalone.d.ts ├── matchers.d.ts └── vitest.d.ts ├── vitest.d.ts └── vitest.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `@testing-library/jest-dom` version: 15 | - `node` version: 16 | - `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | ``` 22 | 23 | What you did: 24 | 25 | What happened: 26 | 27 | 28 | 29 | Reproduction repository: 30 | 31 | 35 | 36 | Problem description: 37 | 38 | Suggested solution: 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Bugs, missing documentation, or unexpected behavior 🤔. 4 | --- 5 | 6 | 20 | 21 | - `@testing-library/jest-dom` version: 22 | - `node` version: 23 | - `jest` (or `vitest`) version: 24 | - `npm` (or `yarn`) version: 25 | 26 | 30 | 31 | ### Relevant code or config: 32 | 33 | ```js 34 | var your => (code) => here; 35 | ``` 36 | 37 | ### What you did: 38 | 39 | 40 | 41 | ### What happened: 42 | 43 | 44 | 45 | ### Reproduction: 46 | 47 | 54 | 55 | ### Problem description: 56 | 57 | 58 | 59 | ### Suggested solution: 60 | 61 | 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: I have a suggestion (and might want to implement myself 🙂)! 4 | --- 5 | 6 | 23 | 24 | ### Describe the feature you'd like: 25 | 26 | 39 | 40 | ### Suggested implementation: 41 | 42 | 43 | 44 | ### Describe alternatives you've considered: 45 | 46 | 50 | 51 | ### Teachability, Documentation, Adoption, Migration Strategy: 52 | 53 | 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support Question 3 | about: 🛑 If you have a question 💬, please check out our support channels! 4 | --- 5 | 6 | ------------ 👆 Click "Preview"! 7 | 8 | Issues on GitHub are intended to be related to problems with the library itself 9 | and feature requests so we recommend not using this medium to ask them here 😁. 10 | 11 | --- 12 | 13 | ## ❓ Support Forums 14 | 15 | - Discord https://discord.gg/testing-library 16 | - Stack Overflow https://stackoverflow.com/questions/tagged/jest-dom 17 | 18 | **ISSUES WHICH ARE QUESTIONS WILL BE CLOSED** 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | **What**: 18 | 19 | 20 | 21 | **Why**: 22 | 23 | 24 | 25 | **How**: 26 | 27 | 28 | 29 | **Checklist**: 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | - [ ] Documentation 38 | - [ ] Tests 39 | - [ ] Updated Type Definitions 40 | - [ ] Ready to be merged 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.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 | jobs: 14 | main: 15 | # ignore all-contributors PRs 16 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 17 | strategy: 18 | matrix: 19 | node: [14, 16, 18, 20] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: ⬇️ Checkout repo 23 | uses: actions/checkout@v2 24 | 25 | - name: ⎔ Setup node 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - name: 📥 Download deps 31 | uses: bahmutov/npm-install@v1 32 | with: 33 | useLockFile: false 34 | 35 | - name: ▶️ Run validate script 36 | run: npm run validate 37 | env: 38 | FORCE_COLOR: true 39 | 40 | - name: ⬆️ Upload coverage report 41 | uses: codecov/codecov-action@v4 42 | with: 43 | fail_ci_if_error: true 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | 46 | release: 47 | needs: main 48 | runs-on: ubuntu-latest 49 | if: 50 | ${{ github.repository == 'testing-library/jest-dom' && 51 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 52 | github.ref) && github.event_name == 'push' }} 53 | steps: 54 | - name: ⬇️ Checkout repo 55 | uses: actions/checkout@v2 56 | 57 | - name: ⎔ Setup node 58 | uses: actions/setup-node@v2 59 | with: 60 | node-version: 14 61 | 62 | - name: 📥 Download deps 63 | uses: bahmutov/npm-install@v1 64 | with: 65 | useLockFile: false 66 | 67 | - name: 🏗 Run build script 68 | run: npm run build 69 | 70 | - name: 🚀 Release 71 | uses: cycjimmy/semantic-release-action@v2 72 | with: 73 | semantic_version: 17 74 | branches: | 75 | [ 76 | '+([0-9])?(.{+([0-9]),x}).x', 77 | 'main', 78 | 'next', 79 | 'next-major', 80 | {name: 'beta', prerelease: true}, 81 | {name: 'alpha', prerelease: true} 82 | ] 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 86 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/husky') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make pull 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/testing-library/jest-dom.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test:update` which will update any snapshots that need updating. Make 33 | sure to include those changes (if they exist) in your commit. 34 | 35 | ## Help needed 36 | 37 | Please checkout the [the open issues][issues] 38 | 39 | Also, please watch the repo and respond to questions/bug reports/feature 40 | requests! Thanks! 41 | 42 | 43 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 44 | [all-contributors]: https://github.com/all-contributors/all-contributors 45 | [issues]: https://github.com/testing-library/jest-dom/issues 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Kent C. Dodds 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 | -------------------------------------------------------------------------------- /jest-globals.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /jest-globals.js: -------------------------------------------------------------------------------- 1 | const globals = require('@jest/globals') 2 | const extensions = require('./dist/matchers') 3 | 4 | globals.expect.extend(extensions) 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('kcd-scripts/jest') 2 | 3 | module.exports = { 4 | ...config, 5 | watchPlugins: [ 6 | ...config.watchPlugins, 7 | require.resolve('jest-watch-select-projects'), 8 | ], 9 | projects: [ 10 | require.resolve('./tests/jest.config.dom'), 11 | require.resolve('./tests/jest.config.node'), 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /matchers.d.ts: -------------------------------------------------------------------------------- 1 | import * as matchers from './types/matchers' 2 | 3 | export = matchers 4 | -------------------------------------------------------------------------------- /matchers.js: -------------------------------------------------------------------------------- 1 | const matchers = require('./dist/matchers') 2 | module.exports = matchers 3 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | TestingLibraryOSS@gmail.com. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /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 github action is kicked off automatically (see 36 | the `.github/workflows/validate.yml` for what runs in the action). We avoid 37 | merging anything that breaks the validate action. 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 | github action 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]: 70 | https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 71 | -------------------------------------------------------------------------------- /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: 3 45 | -------------------------------------------------------------------------------- /other/owl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/jest-dom/918b6fbcde10d4409ee8f05c6e4eecbe96a72b7a/other/owl.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@testing-library/jest-dom", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Custom jest matchers to test the state of the DOM", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "exports": { 8 | ".": { 9 | "require": { 10 | "types": "./types/index.d.ts", 11 | "default": "./dist/index.js" 12 | }, 13 | "import": { 14 | "types": "./types/index.d.ts", 15 | "default": "./dist/index.mjs" 16 | } 17 | }, 18 | "./jest-globals": { 19 | "require": { 20 | "types": "./types/jest-globals.d.ts", 21 | "default": "./dist/jest-globals.js" 22 | }, 23 | "import": { 24 | "types": "./types/jest-globals.d.ts", 25 | "default": "./dist/jest-globals.mjs" 26 | } 27 | }, 28 | "./matchers": { 29 | "require": { 30 | "types": "./types/matchers-standalone.d.ts", 31 | "default": "./dist/matchers.js" 32 | }, 33 | "import": { 34 | "types": "./types/matchers-standalone.d.ts", 35 | "default": "./dist/matchers.mjs" 36 | } 37 | }, 38 | "./vitest": { 39 | "require": { 40 | "types": "./types/vitest.d.ts", 41 | "default": "./dist/vitest.js" 42 | }, 43 | "import": { 44 | "types": "./types/vitest.d.ts", 45 | "default": "./dist/vitest.mjs" 46 | } 47 | }, 48 | "./package.json": "./package.json" 49 | }, 50 | "types": "types/index.d.ts", 51 | "engines": { 52 | "node": ">=14", 53 | "npm": ">=6", 54 | "yarn": ">=1" 55 | }, 56 | "scripts": { 57 | "build": "rollup -c", 58 | "format": "kcd-scripts format", 59 | "lint": "kcd-scripts lint", 60 | "setup": "npm install && npm run validate -s", 61 | "test": "kcd-scripts test", 62 | "test:update": "npm test -- --updateSnapshot --coverage", 63 | "test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest && tsc -p types/__tests__/bun", 64 | "validate": "kcd-scripts validate && npm run test:types" 65 | }, 66 | "files": [ 67 | "dist", 68 | "types", 69 | "*.d.ts", 70 | "jest-globals.js", 71 | "matchers.js", 72 | "vitest.js" 73 | ], 74 | "keywords": [ 75 | "testing", 76 | "dom", 77 | "jest", 78 | "jsdom" 79 | ], 80 | "author": "Ernesto Garcia (http://gnapse.github.io)", 81 | "license": "MIT", 82 | "dependencies": { 83 | "@adobe/css-tools": "^4.4.0", 84 | "aria-query": "^5.0.0", 85 | "chalk": "^3.0.0", 86 | "css.escape": "^1.5.1", 87 | "dom-accessibility-api": "^0.6.3", 88 | "lodash": "^4.17.21", 89 | "redent": "^3.0.0" 90 | }, 91 | "devDependencies": { 92 | "@jest/globals": "^29.6.2", 93 | "@rollup/plugin-commonjs": "^25.0.4", 94 | "@types/bun": "latest", 95 | "@types/web": "latest", 96 | "expect": "^29.6.2", 97 | "jest-environment-jsdom-sixteen": "^1.0.3", 98 | "jest-watch-select-projects": "^2.0.0", 99 | "jsdom": "^16.2.1", 100 | "kcd-scripts": "^14.0.0", 101 | "pretty-format": "^25.1.0", 102 | "rollup": "^3.28.1", 103 | "rollup-plugin-delete": "^2.0.0", 104 | "typescript": "^5.1.6", 105 | "vitest": "^0.34.1" 106 | }, 107 | "eslintConfig": { 108 | "extends": "./node_modules/kcd-scripts/eslint.js", 109 | "parserOptions": { 110 | "sourceType": "module", 111 | "ecmaVersion": 2020 112 | }, 113 | "rules": { 114 | "no-invalid-this": "off" 115 | }, 116 | "overrides": [ 117 | { 118 | "files": [ 119 | "src/__tests__/*.js" 120 | ], 121 | "rules": { 122 | "max-lines-per-function": "off" 123 | } 124 | }, 125 | { 126 | "files": [ 127 | "**/*.d.ts" 128 | ], 129 | "rules": { 130 | "@typescript-eslint/no-empty-interface": "off", 131 | "@typescript-eslint/no-explicit-any": "off", 132 | "@typescript-eslint/no-invalid-void-type": "off", 133 | "@typescript-eslint/no-unused-vars": "off", 134 | "@typescript-eslint/triple-slash-reference": "off" 135 | } 136 | } 137 | ] 138 | }, 139 | "eslintIgnore": [ 140 | "node_modules", 141 | "coverage", 142 | "dist", 143 | "types/__tests__" 144 | ], 145 | "repository": { 146 | "type": "git", 147 | "url": "https://github.com/testing-library/jest-dom" 148 | }, 149 | "bugs": { 150 | "url": "https://github.com/testing-library/jest-dom/issues" 151 | }, 152 | "homepage": "https://github.com/testing-library/jest-dom#readme" 153 | } 154 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const del = require('rollup-plugin-delete') 2 | const commonjs = require('@rollup/plugin-commonjs') 3 | 4 | const entries = [ 5 | './src/index.js', 6 | './src/jest-globals.js', 7 | './src/matchers.js', 8 | './src/vitest.js', 9 | ] 10 | 11 | module.exports = [ 12 | { 13 | input: entries, 14 | output: [ 15 | { 16 | dir: 'dist', 17 | entryFileNames: '[name].mjs', 18 | chunkFileNames: '[name]-[hash].mjs', 19 | format: 'esm', 20 | }, 21 | { 22 | dir: 'dist', 23 | entryFileNames: '[name].js', 24 | chunkFileNames: '[name]-[hash].js', 25 | format: 'cjs', 26 | }, 27 | ], 28 | external: id => 29 | !id.startsWith('\0') && !id.startsWith('.') && !id.startsWith('/'), 30 | plugins: [del({targets: 'dist/*'}), commonjs()], 31 | }, 32 | ] 33 | -------------------------------------------------------------------------------- /src/__tests__/helpers/document.js: -------------------------------------------------------------------------------- 1 | if (global.document) { 2 | module.exports = global.document 3 | } else { 4 | const {JSDOM} = require('jsdom') 5 | const {window} = new JSDOM() 6 | 7 | module.exports = window.document 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/helpers/test-utils.js: -------------------------------------------------------------------------------- 1 | import document from './document' 2 | 3 | function render(html) { 4 | const container = document.createElement('div') 5 | container.innerHTML = html 6 | const queryByTestId = testId => 7 | container.querySelector(`[data-testid="${testId}"]`) 8 | // asFragment has been stolen from react-testing-library 9 | const asFragment = () => 10 | document.createRange().createContextualFragment(container.innerHTML) 11 | 12 | // Some tests need to look up global ids with document.getElementById() 13 | // so we need to be inside an actual document. 14 | document.body.innerHTML = '' 15 | document.body.appendChild(container) 16 | 17 | return {container, queryByTestId, asFragment} 18 | } 19 | 20 | export {render} 21 | -------------------------------------------------------------------------------- /src/__tests__/to-be-empty-dom-element.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('.toBeEmptyDOMElement', () => { 4 | const {queryByTestId} = render(` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Text`) 15 | 16 | const empty = queryByTestId('empty') 17 | const notEmpty = queryByTestId('not-empty') 18 | const svgEmpty = queryByTestId('svg-empty') 19 | const withComment = queryByTestId('with-comment') 20 | const withMultipleComments = queryByTestId('with-multiple-comments') 21 | const withElement = queryByTestId('with-element') 22 | const withElementAndComment = queryByTestId('with-element-and-comment') 23 | const withWhitespace = queryByTestId('with-whitespace') 24 | const withText = queryByTestId('with-whitespace') 25 | const nonExistantElement = queryByTestId('not-exists') 26 | const fakeElement = {thisIsNot: 'an html element'} 27 | 28 | expect(empty).toBeEmptyDOMElement() 29 | expect(svgEmpty).toBeEmptyDOMElement() 30 | expect(notEmpty).not.toBeEmptyDOMElement() 31 | expect(withComment).toBeEmptyDOMElement() 32 | expect(withMultipleComments).toBeEmptyDOMElement() 33 | expect(withElement).not.toBeEmptyDOMElement() 34 | expect(withElementAndComment).not.toBeEmptyDOMElement() 35 | expect(withWhitespace).not.toBeEmptyDOMElement() 36 | expect(withText).not.toBeEmptyDOMElement() 37 | 38 | // negative test cases wrapped in throwError assertions for coverage. 39 | expect(() => expect(empty).not.toBeEmptyDOMElement()).toThrowError() 40 | 41 | expect(() => expect(svgEmpty).not.toBeEmptyDOMElement()).toThrowError() 42 | 43 | expect(() => expect(notEmpty).toBeEmptyDOMElement()).toThrowError() 44 | 45 | expect(() => expect(withComment).not.toBeEmptyDOMElement()).toThrowError() 46 | 47 | expect(() => expect(withMultipleComments).not.toBeEmptyDOMElement()).toThrowError() 48 | 49 | expect(() => expect(withElement).toBeEmptyDOMElement()).toThrowError() 50 | 51 | expect(() => expect(withElementAndComment).toBeEmptyDOMElement()).toThrowError() 52 | 53 | expect(() => expect(withWhitespace).toBeEmptyDOMElement()).toThrowError() 54 | 55 | expect(() => expect(withText).toBeEmptyDOMElement()).toThrowError() 56 | 57 | expect(() => expect(fakeElement).toBeEmptyDOMElement()).toThrowError() 58 | 59 | expect(() => { 60 | expect(nonExistantElement).toBeEmptyDOMElement() 61 | }).toThrowError() 62 | }) 63 | -------------------------------------------------------------------------------- /src/__tests__/to-be-empty.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('.toBeEmpty', () => { 4 | // @deprecated intentionally hiding warnings for test clarity 5 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) 6 | const {queryByTestId} = render(` 7 | 8 | 9 | 10 | `) 11 | 12 | const empty = queryByTestId('empty') 13 | const notEmpty = queryByTestId('not-empty') 14 | const svgEmpty = queryByTestId('svg-empty') 15 | const nonExistantElement = queryByTestId('not-exists') 16 | const fakeElement = {thisIsNot: 'an html element'} 17 | 18 | expect(empty).toBeEmpty() 19 | expect(svgEmpty).toBeEmpty() 20 | expect(notEmpty).not.toBeEmpty() 21 | 22 | // negative test cases wrapped in throwError assertions for coverage. 23 | expect(() => expect(empty).not.toBeEmpty()).toThrowError() 24 | 25 | expect(() => expect(svgEmpty).not.toBeEmpty()).toThrowError() 26 | 27 | expect(() => expect(notEmpty).toBeEmpty()).toThrowError() 28 | 29 | expect(() => expect(fakeElement).toBeEmpty()).toThrowError() 30 | 31 | expect(() => { 32 | expect(nonExistantElement).toBeEmpty() 33 | }).toThrowError() 34 | spy.mockRestore() 35 | }) 36 | -------------------------------------------------------------------------------- /src/__tests__/to-be-in-the-document.js: -------------------------------------------------------------------------------- 1 | import {HtmlElementTypeError} from '../utils' 2 | import document from './helpers/document' 3 | 4 | test('.toBeInTheDocument', () => { 5 | const window = document.defaultView 6 | 7 | window.customElements.define( 8 | 'custom-element', 9 | class extends window.HTMLElement { 10 | constructor() { 11 | super() 12 | this.attachShadow({mode: 'open'}).innerHTML = 13 | '
' 14 | } 15 | }, 16 | ) 17 | 18 | document.body.innerHTML = ` 19 | Html Element 20 | 21 | ` 22 | 23 | const htmlElement = document.querySelector('[data-testid="html-element"]') 24 | const svgElement = document.querySelector('[data-testid="svg-element"]') 25 | const customElementChild = document 26 | .querySelector('[data-testid="custom-element"]') 27 | .shadowRoot.querySelector('[data-testid="custom-element-child"]') 28 | const detachedElement = document.createElement('div') 29 | const fakeElement = {thisIsNot: 'an html element'} 30 | const undefinedElement = undefined 31 | const nullElement = null 32 | 33 | expect(htmlElement).toBeInTheDocument() 34 | expect(svgElement).toBeInTheDocument() 35 | expect(customElementChild).toBeInTheDocument() 36 | expect(detachedElement).not.toBeInTheDocument() 37 | expect(nullElement).not.toBeInTheDocument() 38 | 39 | // negative test cases wrapped in throwError assertions for coverage. 40 | const expectToBe = /expect.*\.toBeInTheDocument/ 41 | const expectNotToBe = /expect.*not\.toBeInTheDocument/ 42 | expect(() => expect(htmlElement).not.toBeInTheDocument()).toThrowError( 43 | expectNotToBe, 44 | ) 45 | expect(() => expect(svgElement).not.toBeInTheDocument()).toThrowError( 46 | expectNotToBe, 47 | ) 48 | expect(() => expect(detachedElement).toBeInTheDocument()).toThrowError( 49 | expectToBe, 50 | ) 51 | expect(() => expect(fakeElement).toBeInTheDocument()).toThrowError( 52 | HtmlElementTypeError, 53 | ) 54 | expect(() => expect(nullElement).toBeInTheDocument()).toThrowError( 55 | HtmlElementTypeError, 56 | ) 57 | expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError( 58 | HtmlElementTypeError, 59 | ) 60 | expect(() => expect(undefinedElement).not.toBeInTheDocument()).toThrowError( 61 | HtmlElementTypeError, 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /src/__tests__/to-be-in-the-dom.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('.toBeInTheDOM', () => { 4 | // @deprecated intentionally hiding warnings for test clarity 5 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) 6 | 7 | const {queryByTestId} = render(` 8 | 9 | 10 | 11 | `) 12 | 13 | const containerElement = queryByTestId('count-container') 14 | const valueElement = queryByTestId('count-value') 15 | const nonExistantElement = queryByTestId('not-exists') 16 | const svgElement = queryByTestId('svg-element') 17 | const fakeElement = {thisIsNot: 'an html element'} 18 | 19 | // Testing toBeInTheDOM without container 20 | expect(valueElement).toBeInTheDOM() 21 | expect(svgElement).toBeInTheDOM() 22 | expect(nonExistantElement).not.toBeInTheDOM() 23 | 24 | // negative test cases wrapped in throwError assertions for coverage. 25 | expect(() => expect(valueElement).not.toBeInTheDOM()).toThrowError() 26 | 27 | expect(() => expect(svgElement).not.toBeInTheDOM()).toThrowError() 28 | 29 | expect(() => expect(nonExistantElement).toBeInTheDOM()).toThrowError() 30 | 31 | expect(() => expect(fakeElement).toBeInTheDOM()).toThrowError() 32 | 33 | // Testing toBeInTheDOM with container 34 | expect(valueElement).toBeInTheDOM(containerElement) 35 | expect(svgElement).toBeInTheDOM(containerElement) 36 | expect(containerElement).not.toBeInTheDOM(valueElement) 37 | 38 | expect(() => 39 | expect(valueElement).not.toBeInTheDOM(containerElement), 40 | ).toThrowError() 41 | 42 | expect(() => 43 | expect(svgElement).not.toBeInTheDOM(containerElement), 44 | ).toThrowError() 45 | 46 | expect(() => 47 | expect(nonExistantElement).toBeInTheDOM(containerElement), 48 | ).toThrowError() 49 | 50 | expect(() => 51 | expect(fakeElement).toBeInTheDOM(containerElement), 52 | ).toThrowError() 53 | 54 | expect(() => { 55 | expect(valueElement).toBeInTheDOM(fakeElement) 56 | }).toThrowError() 57 | 58 | spy.mockRestore() 59 | }) 60 | -------------------------------------------------------------------------------- /src/__tests__/to-be-invalid.js: -------------------------------------------------------------------------------- 1 | import {JSDOM} from 'jsdom' 2 | import {render} from './helpers/test-utils' 3 | 4 | /* 5 | * This function is being used to test if `.toBeInvalid` and `.toBeValid` 6 | * are correctly triggered by the DOM Node method `.checkValidity()`, part 7 | * of the Web API. 8 | * 9 | * For this check, we are using the `jsdom` library to return a DOM Node 10 | * sending the good information to our test. 11 | * 12 | * We are using this library because without it `.checkValidity()` returns 13 | * always `true` when using `yarn test` in a terminal. 14 | * 15 | * Please consult the PR 110 to get more information: 16 | * https://github.com/testing-library/jest-dom/pull/110 17 | * 18 | * @link https://github.com/testing-library/jest-dom/pull/110 19 | * @link https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation 20 | * @link https://github.com/jsdom/jsdom 21 | */ 22 | function getDOMElement(htmlString, selector) { 23 | return new JSDOM(htmlString).window.document.querySelector(selector) 24 | } 25 | 26 | // A required field without a value is invalid 27 | const invalidInputHtml = `` 28 | 29 | const invalidInputNode = getDOMElement(invalidInputHtml, 'input') 30 | 31 | // A form is invalid if it contains an invalid input 32 | const invalidFormHtml = `
${invalidInputHtml}
` 33 | 34 | const invalidFormNode = getDOMElement(invalidFormHtml, 'form') 35 | 36 | describe('.toBeInvalid', () => { 37 | test('handles ', () => { 38 | const {queryByTestId} = render(` 39 |
40 | 41 | 42 | 43 | 44 |
45 | `) 46 | 47 | expect(queryByTestId('no-aria-invalid')).not.toBeInvalid() 48 | expect(queryByTestId('aria-invalid')).toBeInvalid() 49 | expect(queryByTestId('aria-invalid-value')).toBeInvalid() 50 | expect(queryByTestId('aria-invalid-false')).not.toBeInvalid() 51 | expect(invalidInputNode).toBeInvalid() 52 | 53 | // negative test cases wrapped in throwError assertions for coverage. 54 | expect(() => 55 | expect(queryByTestId('no-aria-invalid')).toBeInvalid(), 56 | ).toThrowError() 57 | expect(() => 58 | expect(queryByTestId('aria-invalid')).not.toBeInvalid(), 59 | ).toThrowError() 60 | expect(() => 61 | expect(queryByTestId('aria-invalid-value')).not.toBeInvalid(), 62 | ).toThrowError() 63 | expect(() => 64 | expect(queryByTestId('aria-invalid-false')).toBeInvalid(), 65 | ).toThrowError() 66 | expect(() => expect(invalidInputNode).not.toBeInvalid()).toThrowError() 67 | }) 68 | 69 | test('handles
', () => { 70 | const {queryByTestId} = render(` 71 | 72 | 73 |
74 | `) 75 | 76 | expect(queryByTestId('valid')).not.toBeInvalid() 77 | expect(invalidFormNode).toBeInvalid() 78 | 79 | // negative test cases wrapped in throwError assertions for coverage. 80 | expect(() => expect(queryByTestId('valid')).toBeInvalid()).toThrowError() 81 | expect(() => expect(invalidFormNode).not.toBeInvalid()).toThrowError() 82 | }) 83 | 84 | test('handles any element', () => { 85 | const {queryByTestId} = render(` 86 |
    87 |
  1. 88 |
  2. 89 |
  3. 90 |
  4. 91 |
92 | `) 93 | 94 | expect(queryByTestId('valid')).not.toBeInvalid() 95 | expect(queryByTestId('no-aria-invalid')).not.toBeInvalid() 96 | expect(queryByTestId('aria-invalid')).toBeInvalid() 97 | expect(queryByTestId('aria-invalid-value')).toBeInvalid() 98 | expect(queryByTestId('aria-invalid-false')).not.toBeInvalid() 99 | 100 | // negative test cases wrapped in throwError assertions for coverage. 101 | expect(() => expect(queryByTestId('valid')).toBeInvalid()).toThrowError() 102 | expect(() => 103 | expect(queryByTestId('no-aria-invalid')).toBeInvalid(), 104 | ).toThrowError() 105 | expect(() => 106 | expect(queryByTestId('aria-invalid')).not.toBeInvalid(), 107 | ).toThrowError() 108 | expect(() => 109 | expect(queryByTestId('aria-invalid-value')).not.toBeInvalid(), 110 | ).toThrowError() 111 | expect(() => 112 | expect(queryByTestId('aria-invalid-false')).toBeInvalid(), 113 | ).toThrowError() 114 | }) 115 | }) 116 | 117 | describe('.toBeValid', () => { 118 | test('handles ', () => { 119 | const {queryByTestId} = render(` 120 |
121 | 122 | 123 | 124 | 125 |
126 | `) 127 | 128 | expect(queryByTestId('no-aria-invalid')).toBeValid() 129 | expect(queryByTestId('aria-invalid')).not.toBeValid() 130 | expect(queryByTestId('aria-invalid-value')).not.toBeValid() 131 | expect(queryByTestId('aria-invalid-false')).toBeValid() 132 | expect(invalidInputNode).not.toBeValid() 133 | 134 | // negative test cases wrapped in throwError assertions for coverage. 135 | expect(() => 136 | expect(queryByTestId('no-aria-invalid')).not.toBeValid(), 137 | ).toThrowError() 138 | expect(() => 139 | expect(queryByTestId('aria-invalid')).toBeValid(), 140 | ).toThrowError() 141 | expect(() => 142 | expect(queryByTestId('aria-invalid-value')).toBeValid(), 143 | ).toThrowError() 144 | expect(() => 145 | expect(queryByTestId('aria-invalid-false')).not.toBeValid(), 146 | ).toThrowError() 147 | expect(() => expect(invalidInputNode).toBeValid()).toThrowError() 148 | }) 149 | 150 | test('handles
', () => { 151 | const {queryByTestId} = render(` 152 | 153 | 154 |
155 | `) 156 | 157 | expect(queryByTestId('valid')).toBeValid() 158 | expect(invalidFormNode).not.toBeValid() 159 | 160 | // negative test cases wrapped in throwError assertions for coverage. 161 | expect(() => expect(queryByTestId('valid')).not.toBeValid()).toThrowError() 162 | expect(() => expect(invalidFormNode).toBeValid()).toThrowError() 163 | }) 164 | 165 | test('handles any element', () => { 166 | const {queryByTestId} = render(` 167 |
    168 |
  1. 169 |
  2. 170 |
  3. 171 |
  4. 172 |
173 | `) 174 | 175 | expect(queryByTestId('valid')).toBeValid() 176 | expect(queryByTestId('no-aria-invalid')).toBeValid() 177 | expect(queryByTestId('aria-invalid')).not.toBeValid() 178 | expect(queryByTestId('aria-invalid-value')).not.toBeValid() 179 | expect(queryByTestId('aria-invalid-false')).toBeValid() 180 | 181 | // negative test cases wrapped in throwError assertions for coverage. 182 | expect(() => expect(queryByTestId('valid')).not.toBeValid()).toThrowError() 183 | expect(() => 184 | expect(queryByTestId('no-aria-invalid')).not.toBeValid(), 185 | ).toThrowError() 186 | expect(() => 187 | expect(queryByTestId('aria-invalid')).toBeValid(), 188 | ).toThrowError() 189 | expect(() => 190 | expect(queryByTestId('aria-invalid-value')).toBeValid(), 191 | ).toThrowError() 192 | expect(() => 193 | expect(queryByTestId('aria-invalid-false')).not.toBeValid(), 194 | ).toThrowError() 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /src/__tests__/to-be-partially-checked.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toBePartiallyChecked', () => { 4 | test('handles input checkbox with aria-checked', () => { 5 | const {queryByTestId} = render(` 6 | 7 | 8 | 9 | `) 10 | 11 | expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked() 12 | expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked() 13 | expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked() 14 | }) 15 | 16 | test('handles input checkbox set as indeterminate', () => { 17 | const {queryByTestId} = render(` 18 | 19 | 20 | 21 | `) 22 | 23 | queryByTestId('checkbox-mixed').indeterminate = true 24 | 25 | expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked() 26 | expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked() 27 | expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked() 28 | }) 29 | 30 | test('handles element with role="checkbox"', () => { 31 | const {queryByTestId} = render(` 32 |
33 |
34 |
35 | `) 36 | 37 | expect(queryByTestId('aria-checkbox-mixed')).toBePartiallyChecked() 38 | expect(queryByTestId('aria-checkbox-checked')).not.toBePartiallyChecked() 39 | expect(queryByTestId('aria-checkbox-unchecked')).not.toBePartiallyChecked() 40 | }) 41 | 42 | test('throws when input checkbox is mixed but expected not to be', () => { 43 | const {queryByTestId} = render( 44 | ``, 45 | ) 46 | 47 | expect(() => 48 | expect(queryByTestId('checkbox-mixed')).not.toBePartiallyChecked(), 49 | ).toThrowError() 50 | }) 51 | 52 | test('throws when input checkbox is indeterminate but expected not to be', () => { 53 | const {queryByTestId} = render( 54 | ``, 55 | ) 56 | 57 | queryByTestId('checkbox-mixed').indeterminate = true 58 | 59 | expect(() => 60 | expect(queryByTestId('input-mixed')).not.toBePartiallyChecked(), 61 | ).toThrowError() 62 | }) 63 | 64 | test('throws when input checkbox is not checked but expected to be', () => { 65 | const {queryByTestId} = render( 66 | ``, 67 | ) 68 | 69 | expect(() => 70 | expect(queryByTestId('checkbox-empty')).toBePartiallyChecked(), 71 | ).toThrowError() 72 | }) 73 | 74 | test('throws when element with role="checkbox" is partially checked but expected not to be', () => { 75 | const {queryByTestId} = render( 76 | `
`, 77 | ) 78 | 79 | expect(() => 80 | expect(queryByTestId('aria-checkbox-mixed')).not.toBePartiallyChecked(), 81 | ).toThrowError() 82 | }) 83 | 84 | test('throws when element with role="checkbox" is checked but expected to be partially checked', () => { 85 | const {queryByTestId} = render( 86 | `
`, 87 | ) 88 | 89 | expect(() => 90 | expect(queryByTestId('aria-checkbox-checked')).toBePartiallyChecked(), 91 | ).toThrowError() 92 | }) 93 | 94 | test('throws when element with role="checkbox" is not checked but expected to be', () => { 95 | const {queryByTestId} = render( 96 | `
`, 97 | ) 98 | 99 | expect(() => 100 | expect(queryByTestId('aria-checkbox')).toBePartiallyChecked(), 101 | ).toThrowError() 102 | }) 103 | 104 | test('throws when element with role="checkbox" has an invalid aria-checked attribute', () => { 105 | const {queryByTestId} = render( 106 | `
`, 107 | ) 108 | 109 | expect(() => 110 | expect(queryByTestId('aria-checkbox-invalid')).toBePartiallyChecked(), 111 | ).toThrowError() 112 | }) 113 | 114 | test('throws when the element is not a checkbox', () => { 115 | const {queryByTestId} = render(``) 116 | expect(() => 117 | expect(queryByTestId('select')).toBePartiallyChecked(), 118 | ).toThrowError( 119 | 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead', 120 | ) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /src/__tests__/to-be-required.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('.toBeRequired', () => { 4 | const {queryByTestId} = render(` 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | `) 18 | 19 | expect(queryByTestId('required-input')).toBeRequired() 20 | expect(queryByTestId('aria-required-input')).toBeRequired() 21 | expect(queryByTestId('conflicted-input')).toBeRequired() 22 | expect(queryByTestId('not-required-input')).not.toBeRequired() 23 | expect(queryByTestId('basic-input')).not.toBeRequired() 24 | expect(queryByTestId('unsupported-type')).not.toBeRequired() 25 | expect(queryByTestId('select')).toBeRequired() 26 | expect(queryByTestId('textarea')).toBeRequired() 27 | expect(queryByTestId('supported-role')).not.toBeRequired() 28 | expect(queryByTestId('supported-role-aria')).toBeRequired() 29 | 30 | // negative test cases wrapped in throwError assertions for coverage. 31 | expect(() => 32 | expect(queryByTestId('required-input')).not.toBeRequired(), 33 | ).toThrowError() 34 | expect(() => 35 | expect(queryByTestId('aria-required-input')).not.toBeRequired(), 36 | ).toThrowError() 37 | expect(() => 38 | expect(queryByTestId('conflicted-input')).not.toBeRequired(), 39 | ).toThrowError() 40 | expect(() => 41 | expect(queryByTestId('not-required-input')).toBeRequired(), 42 | ).toThrowError() 43 | expect(() => 44 | expect(queryByTestId('basic-input')).toBeRequired(), 45 | ).toThrowError() 46 | expect(() => 47 | expect(queryByTestId('unsupported-type')).toBeRequired(), 48 | ).toThrowError() 49 | expect(() => 50 | expect(queryByTestId('select')).not.toBeRequired(), 51 | ).toThrowError() 52 | expect(() => 53 | expect(queryByTestId('textarea')).not.toBeRequired(), 54 | ).toThrowError() 55 | expect(() => 56 | expect(queryByTestId('supported-role')).toBeRequired(), 57 | ).toThrowError() 58 | expect(() => 59 | expect(queryByTestId('supported-role-aria')).not.toBeRequired(), 60 | ).toThrowError() 61 | }) 62 | -------------------------------------------------------------------------------- /src/__tests__/to-contain-element.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | const {queryByTestId} = render(` 4 | 5 | 6 | 7 | 8 | 9 | 10 | `) 11 | 12 | const grandparent = queryByTestId('grandparent') 13 | const parent = queryByTestId('parent') 14 | const child = queryByTestId('child') 15 | const svgElement = queryByTestId('svg-element') 16 | const nonExistantElement = queryByTestId('not-exists') 17 | const fakeElement = {thisIsNot: 'an html element'} 18 | 19 | test('.toContainElement positive test cases', () => { 20 | expect(grandparent).toContainElement(parent) 21 | expect(grandparent).toContainElement(child) 22 | expect(grandparent).toContainElement(svgElement) 23 | expect(parent).toContainElement(child) 24 | expect(parent).not.toContainElement(grandparent) 25 | expect(parent).not.toContainElement(svgElement) 26 | expect(child).not.toContainElement(parent) 27 | expect(child).not.toContainElement(grandparent) 28 | expect(child).not.toContainElement(svgElement) 29 | expect(grandparent).not.toContainElement(nonExistantElement) 30 | }) 31 | 32 | test('.toContainElement negative test cases', () => { 33 | expect(() => 34 | expect(nonExistantElement).not.toContainElement(child), 35 | ).toThrowError() 36 | expect(() => expect(parent).toContainElement(grandparent)).toThrowError() 37 | expect(() => 38 | expect(nonExistantElement).toContainElement(grandparent), 39 | ).toThrowError() 40 | expect(() => 41 | expect(grandparent).toContainElement(nonExistantElement), 42 | ).toThrowError() 43 | expect(() => 44 | expect(nonExistantElement).toContainElement(nonExistantElement), 45 | ).toThrowError() 46 | expect(() => 47 | expect(nonExistantElement).toContainElement(fakeElement), 48 | ).toThrowError() 49 | expect(() => 50 | expect(fakeElement).toContainElement(nonExistantElement), 51 | ).toThrowError() 52 | expect(() => 53 | expect(fakeElement).not.toContainElement(nonExistantElement), 54 | ).toThrowError() 55 | expect(() => expect(fakeElement).toContainElement(grandparent)).toThrowError() 56 | expect(() => expect(grandparent).toContainElement(fakeElement)).toThrowError() 57 | expect(() => expect(fakeElement).toContainElement(fakeElement)).toThrowError() 58 | expect(() => expect(grandparent).not.toContainElement(child)).toThrowError() 59 | expect(() => 60 | expect(grandparent).not.toContainElement(svgElement), 61 | ).toThrowError() 62 | expect(() => 63 | expect(grandparent).not.toContainElement(undefined), 64 | ).toThrowError() 65 | }) 66 | -------------------------------------------------------------------------------- /src/__tests__/to-contain-html.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | /* eslint-disable max-statements */ 4 | describe('.toContainHTML', () => { 5 | test('handles positive and negative cases', () => { 6 | const {queryByTestId} = render(` 7 | 8 | 9 | 10 | 11 | 12 | 13 | `) 14 | 15 | const grandparent = queryByTestId('grandparent') 16 | const parent = queryByTestId('parent') 17 | const child = queryByTestId('child') 18 | const nonExistantElement = queryByTestId('not-exists') 19 | const fakeElement = {thisIsNot: 'an html element'} 20 | const stringChildElement = '' 21 | const stringChildElementSelfClosing = '' 22 | const incorrectStringHtml = '
' 23 | const nonExistantString = ' Does not exists ' 24 | const svgElement = queryByTestId('svg-element') 25 | 26 | expect(grandparent).toContainHTML(stringChildElement) 27 | expect(parent).toContainHTML(stringChildElement) 28 | expect(child).toContainHTML(stringChildElement) 29 | expect(child).toContainHTML(stringChildElementSelfClosing) 30 | expect(grandparent).not.toContainHTML(nonExistantString) 31 | expect(parent).not.toContainHTML(nonExistantString) 32 | expect(child).not.toContainHTML(nonExistantString) 33 | expect(child).not.toContainHTML(nonExistantString) 34 | expect(grandparent).toContainHTML(incorrectStringHtml) 35 | expect(parent).toContainHTML(incorrectStringHtml) 36 | expect(child).toContainHTML(incorrectStringHtml) 37 | 38 | // negative test cases wrapped in throwError assertions for coverage. 39 | expect(() => 40 | expect(nonExistantElement).not.toContainHTML(stringChildElement), 41 | ).toThrowError() 42 | expect(() => 43 | expect(nonExistantElement).not.toContainHTML(nonExistantElement), 44 | ).toThrowError() 45 | expect(() => 46 | expect(stringChildElement).not.toContainHTML(fakeElement), 47 | ).toThrowError() 48 | expect(() => 49 | expect(svgElement).toContainHTML(stringChildElement), 50 | ).toThrowError() 51 | expect(() => 52 | expect(grandparent).not.toContainHTML(stringChildElement), 53 | ).toThrowError() 54 | expect(() => 55 | expect(parent).not.toContainHTML(stringChildElement), 56 | ).toThrowError() 57 | expect(() => 58 | expect(child).not.toContainHTML(stringChildElement), 59 | ).toThrowError() 60 | expect(() => 61 | expect(child).not.toContainHTML(stringChildElement), 62 | ).toThrowError() 63 | expect(() => 64 | expect(child).not.toContainHTML(stringChildElementSelfClosing), 65 | ).toThrowError() 66 | expect(() => expect(child).toContainHTML(nonExistantString)).toThrowError() 67 | expect(() => expect(parent).toContainHTML(nonExistantString)).toThrowError() 68 | expect(() => 69 | expect(grandparent).toContainHTML(nonExistantString), 70 | ).toThrowError() 71 | expect(() => expect(child).toContainHTML(nonExistantElement)).toThrowError() 72 | expect(() => 73 | expect(parent).toContainHTML(nonExistantElement), 74 | ).toThrowError() 75 | expect(() => 76 | expect(grandparent).toContainHTML(nonExistantElement), 77 | ).toThrowError() 78 | expect(() => 79 | expect(nonExistantElement).not.toContainHTML(incorrectStringHtml), 80 | ).toThrowError() 81 | expect(() => 82 | expect(grandparent).not.toContainHTML(incorrectStringHtml), 83 | ).toThrowError() 84 | expect(() => 85 | expect(child).not.toContainHTML(incorrectStringHtml), 86 | ).toThrowError() 87 | expect(() => 88 | expect(parent).not.toContainHTML(incorrectStringHtml), 89 | ).toThrowError() 90 | }) 91 | 92 | test('throws with an expected text', () => { 93 | const {queryByTestId} = render('') 94 | const htmlElement = queryByTestId('child') 95 | const nonExistantString = '
non-existant element
' 96 | 97 | let errorMessage 98 | try { 99 | expect(htmlElement).toContainHTML(nonExistantString) 100 | } catch (error) { 101 | errorMessage = error.message 102 | } 103 | 104 | expect(errorMessage).toMatchInlineSnapshot(` 105 | expect(element).toContainHTML() 106 | Expected: 107 |
non-existant element
108 | Received: 109 | 110 | `) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /src/__tests__/to-have-accessible-description.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toHaveAccessibleDescription', () => { 4 | it('works with the link title attribute', () => { 5 | const {queryByTestId} = render(` 6 |
7 | Start 8 | About 9 |
10 | `) 11 | 12 | const link = queryByTestId('link') 13 | expect(link).toHaveAccessibleDescription() 14 | expect(link).toHaveAccessibleDescription('A link to start over') 15 | expect(link).not.toHaveAccessibleDescription('Home page') 16 | expect(() => { 17 | expect(link).toHaveAccessibleDescription('Invalid description') 18 | }).toThrow(/expected element to have accessible description/i) 19 | expect(() => { 20 | expect(link).not.toHaveAccessibleDescription() 21 | }).toThrow(/expected element not to have accessible description/i) 22 | 23 | const extraLink = queryByTestId('extra-link') 24 | expect(extraLink).not.toHaveAccessibleDescription() 25 | expect(() => { 26 | expect(extraLink).toHaveAccessibleDescription() 27 | }).toThrow(/expected element to have accessible description/i) 28 | }) 29 | 30 | it('works with aria-describedby attributes', () => { 31 | const {queryByTestId} = render(` 32 |
33 | User profile pic 34 | Company logo 35 | The logo of Our Company 36 |
37 | `) 38 | 39 | const avatar = queryByTestId('avatar') 40 | expect(avatar).not.toHaveAccessibleDescription() 41 | expect(() => { 42 | expect(avatar).toHaveAccessibleDescription('User profile pic') 43 | }).toThrow(/expected element to have accessible description/i) 44 | 45 | const logo = queryByTestId('logo') 46 | expect(logo).not.toHaveAccessibleDescription('Company logo') 47 | expect(logo).toHaveAccessibleDescription('The logo of Our Company') 48 | expect(logo).toHaveAccessibleDescription(/logo of our company/i) 49 | expect(logo).toHaveAccessibleDescription( 50 | expect.stringContaining('logo of Our Company'), 51 | ) 52 | expect(() => { 53 | expect(logo).toHaveAccessibleDescription("Our company's logo") 54 | }).toThrow(/expected element to have accessible description/i) 55 | expect(() => { 56 | expect(logo).not.toHaveAccessibleDescription('The logo of Our Company') 57 | }).toThrow(/expected element not to have accessible description/i) 58 | }) 59 | 60 | it('works with aria-description attribute', () => { 61 | const {queryByTestId} = render(` 62 | Company logo 63 | `) 64 | 65 | const logo = queryByTestId('logo') 66 | expect(logo).not.toHaveAccessibleDescription('Company logo') 67 | expect(logo).toHaveAccessibleDescription('The logo of Our Company') 68 | expect(logo).toHaveAccessibleDescription(/logo of our company/i) 69 | expect(logo).toHaveAccessibleDescription( 70 | expect.stringContaining('logo of Our Company'), 71 | ) 72 | expect(() => { 73 | expect(logo).toHaveAccessibleDescription("Our company's logo") 74 | }).toThrow(/expected element to have accessible description/i) 75 | expect(() => { 76 | expect(logo).not.toHaveAccessibleDescription('The logo of Our Company') 77 | }).toThrow(/expected element not to have accessible description/i) 78 | }) 79 | 80 | it('handles multiple ids', () => { 81 | const {queryByTestId} = render(` 82 |
83 |
First description
84 |
Second description
85 |
Third description
86 | 87 |
88 |
89 | `) 90 | 91 | expect(queryByTestId('multiple')).toHaveAccessibleDescription( 92 | 'First description Second description Third description', 93 | ) 94 | expect(queryByTestId('multiple')).toHaveAccessibleDescription( 95 | /Second description Third/, 96 | ) 97 | expect(queryByTestId('multiple')).toHaveAccessibleDescription( 98 | expect.stringContaining('Second description Third'), 99 | ) 100 | expect(queryByTestId('multiple')).toHaveAccessibleDescription( 101 | expect.stringMatching(/Second description Third/), 102 | ) 103 | expect(queryByTestId('multiple')).not.toHaveAccessibleDescription( 104 | 'Something else', 105 | ) 106 | expect(queryByTestId('multiple')).not.toHaveAccessibleDescription('First') 107 | }) 108 | 109 | it('normalizes whitespace', () => { 110 | const {queryByTestId} = render(` 111 |
112 | Step 113 | 1 114 | of 115 | 4 116 |
117 |
118 | And 119 | extra 120 | description 121 |
122 |
123 | `) 124 | 125 | expect(queryByTestId('target')).toHaveAccessibleDescription( 126 | 'Step 1 of 4 And extra description', 127 | ) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /src/__tests__/to-have-attribute.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('.toHaveAttribute', () => { 4 | const {queryByTestId} = render(` 5 | 8 | 9 | `) 10 | 11 | expect(queryByTestId('ok-button')).toHaveAttribute('disabled') 12 | expect(queryByTestId('ok-button')).toHaveAttribute('type') 13 | expect(queryByTestId('ok-button')).not.toHaveAttribute('class') 14 | expect(queryByTestId('ok-button')).toHaveAttribute('type', 'submit') 15 | expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'button') 16 | expect(queryByTestId('svg-element')).toHaveAttribute('width') 17 | expect(queryByTestId('svg-element')).toHaveAttribute('width', '12') 18 | expect(queryByTestId('ok-button')).not.toHaveAttribute('height') 19 | 20 | expect(() => 21 | expect(queryByTestId('ok-button')).not.toHaveAttribute('disabled'), 22 | ).toThrowError() 23 | expect(() => 24 | expect(queryByTestId('ok-button')).not.toHaveAttribute('type'), 25 | ).toThrowError() 26 | expect(() => 27 | expect(queryByTestId('ok-button')).toHaveAttribute('class'), 28 | ).toThrowError() 29 | expect(() => 30 | expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'submit'), 31 | ).toThrowError() 32 | expect(() => 33 | expect(queryByTestId('ok-button')).toHaveAttribute('type', 'button'), 34 | ).toThrowError() 35 | expect(() => 36 | expect(queryByTestId('svg-element')).not.toHaveAttribute('width'), 37 | ).toThrowError() 38 | expect(() => 39 | expect(queryByTestId('svg-element')).not.toHaveAttribute('width', '12'), 40 | ).toThrowError() 41 | expect(() => 42 | expect({thisIsNot: 'an html element'}).not.toHaveAttribute(), 43 | ).toThrowError() 44 | 45 | // Asymmetric matchers 46 | expect(queryByTestId('ok-button')).toHaveAttribute( 47 | 'type', 48 | expect.stringContaining('sub'), 49 | ) 50 | expect(queryByTestId('ok-button')).toHaveAttribute( 51 | 'type', 52 | expect.stringMatching(/sub*/), 53 | ) 54 | expect(queryByTestId('ok-button')).toHaveAttribute('type', expect.anything()) 55 | 56 | expect(() => 57 | expect(queryByTestId('ok-button')).toHaveAttribute( 58 | 'type', 59 | expect.not.stringContaining('sub'), 60 | ), 61 | ).toThrowError() 62 | }) 63 | -------------------------------------------------------------------------------- /src/__tests__/to-have-description.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toHaveDescription', () => { 4 | let spy 5 | beforeAll(() => { 6 | // @deprecated intentionally hiding warnings for test clarity 7 | spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) 8 | }) 9 | 10 | afterAll(() => { 11 | spy.mockRestore() 12 | }) 13 | 14 | test('handles positive test cases', () => { 15 | const {queryByTestId} = render(` 16 |
The description
17 | 18 |
19 |
20 |
21 | `) 22 | 23 | expect(queryByTestId('single')).toHaveDescription('The description') 24 | expect(queryByTestId('single')).toHaveDescription( 25 | expect.stringContaining('The'), 26 | ) 27 | expect(queryByTestId('single')).toHaveDescription(/The/) 28 | expect(queryByTestId('single')).toHaveDescription( 29 | expect.stringMatching(/The/), 30 | ) 31 | expect(queryByTestId('single')).toHaveDescription(/description/) 32 | expect(queryByTestId('single')).not.toHaveDescription('Something else') 33 | expect(queryByTestId('single')).not.toHaveDescription('The') 34 | 35 | expect(queryByTestId('invalid_id')).not.toHaveDescription() 36 | expect(queryByTestId('invalid_id')).toHaveDescription('') 37 | 38 | expect(queryByTestId('without')).not.toHaveDescription() 39 | expect(queryByTestId('without')).toHaveDescription('') 40 | }) 41 | 42 | test('handles multiple ids', () => { 43 | const {queryByTestId} = render(` 44 |
First description
45 |
Second description
46 |
Third description
47 | 48 |
49 | `) 50 | 51 | expect(queryByTestId('multiple')).toHaveDescription( 52 | 'First description Second description Third description', 53 | ) 54 | expect(queryByTestId('multiple')).toHaveDescription( 55 | /Second description Third/, 56 | ) 57 | expect(queryByTestId('multiple')).toHaveDescription( 58 | expect.stringContaining('Second description Third'), 59 | ) 60 | expect(queryByTestId('multiple')).toHaveDescription( 61 | expect.stringMatching(/Second description Third/), 62 | ) 63 | expect(queryByTestId('multiple')).not.toHaveDescription('Something else') 64 | expect(queryByTestId('multiple')).not.toHaveDescription('First') 65 | }) 66 | 67 | test('handles negative test cases', () => { 68 | const {queryByTestId} = render(` 69 |
The description
70 |
71 | `) 72 | 73 | expect(() => 74 | expect(queryByTestId('other')).toHaveDescription('The description'), 75 | ).toThrowError() 76 | 77 | expect(() => 78 | expect(queryByTestId('target')).toHaveDescription('Something else'), 79 | ).toThrowError() 80 | 81 | expect(() => 82 | expect(queryByTestId('target')).not.toHaveDescription('The description'), 83 | ).toThrowError() 84 | }) 85 | 86 | test('normalizes whitespace', () => { 87 | const {queryByTestId} = render(` 88 |
89 | Step 90 | 1 91 | of 92 | 4 93 |
94 |
95 | And 96 | extra 97 | description 98 |
99 |
100 | `) 101 | 102 | expect(queryByTestId('target')).toHaveDescription( 103 | 'Step 1 of 4 And extra description', 104 | ) 105 | }) 106 | 107 | test('can handle multiple levels with content spread across decendants', () => { 108 | const {queryByTestId} = render(` 109 | 110 | Step 111 | 1 112 | of 113 | 114 | 115 | 4 116 |
117 |
118 | `) 119 | 120 | expect(queryByTestId('target')).toHaveDescription('Step 1 of 4') 121 | }) 122 | 123 | test('handles extra whitespace with multiple ids', () => { 124 | const {queryByTestId} = render(` 125 |
First description
126 |
Second description
127 |
Third description
128 | 129 |
132 | `) 133 | 134 | expect(queryByTestId('multiple')).toHaveDescription( 135 | 'First description Second description Third description', 136 | ) 137 | }) 138 | 139 | test('is case-sensitive', () => { 140 | const {queryByTestId} = render(` 141 | Sensitive text 142 |
143 | `) 144 | 145 | expect(queryByTestId('target')).toHaveDescription('Sensitive text') 146 | expect(queryByTestId('target')).not.toHaveDescription('sensitive text') 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /src/__tests__/to-have-display-value.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | test('it should work as expected', () => { 4 | const {queryByTestId} = render(` 5 | 11 | `) 12 | 13 | expect(queryByTestId('select')).toHaveDisplayValue('Select a fruit...') 14 | expect(queryByTestId('select')).not.toHaveDisplayValue('Select') 15 | expect(queryByTestId('select')).not.toHaveDisplayValue('Banana') 16 | expect(() => 17 | expect(queryByTestId('select')).not.toHaveDisplayValue('Select a fruit...'), 18 | ).toThrow() 19 | expect(() => 20 | expect(queryByTestId('select')).toHaveDisplayValue('Ananas'), 21 | ).toThrow() 22 | 23 | queryByTestId('select').value = 'banana' 24 | expect(queryByTestId('select')).toHaveDisplayValue('Banana') 25 | expect(queryByTestId('select')).toHaveDisplayValue(/[bB]ana/) 26 | }) 27 | 28 | describe('with multiple select', () => { 29 | function mount() { 30 | return render(` 31 | 37 | `) 38 | } 39 | 40 | it('matches only when all the multiple selected values are equal to all the expected values', () => { 41 | const subject = mount() 42 | expect(subject.queryByTestId('select')).toHaveDisplayValue([ 43 | 'Ananas', 44 | 'Avocado', 45 | ]) 46 | expect(() => 47 | expect(subject.queryByTestId('select')).not.toHaveDisplayValue([ 48 | 'Ananas', 49 | 'Avocado', 50 | ]), 51 | ).toThrow() 52 | expect(subject.queryByTestId('select')).not.toHaveDisplayValue([ 53 | 'Ananas', 54 | 'Avocado', 55 | 'Orange', 56 | ]) 57 | expect(subject.queryByTestId('select')).not.toHaveDisplayValue('Ananas') 58 | expect(() => 59 | expect(subject.queryByTestId('select')).toHaveDisplayValue('Ananas'), 60 | ).toThrow() 61 | 62 | Array.from(subject.queryByTestId('select').options).forEach(option => { 63 | option.selected = ['ananas', 'banana'].includes(option.value) 64 | }) 65 | 66 | expect(subject.queryByTestId('select')).toHaveDisplayValue([ 67 | 'Ananas', 68 | 'Banana', 69 | ]) 70 | }) 71 | 72 | it('matches even when the expected values are unordered', () => { 73 | const subject = mount() 74 | expect(subject.queryByTestId('select')).toHaveDisplayValue([ 75 | 'Avocado', 76 | 'Ananas', 77 | ]) 78 | }) 79 | 80 | it('matches with regex expected values', () => { 81 | const subject = mount() 82 | expect(subject.queryByTestId('select')).toHaveDisplayValue([ 83 | /[Aa]nanas/, 84 | 'Avocado', 85 | ]) 86 | }) 87 | }) 88 | 89 | test('it should work with input elements', () => { 90 | const {queryByTestId} = render(` 91 | 92 | `) 93 | 94 | expect(queryByTestId('input')).toHaveDisplayValue('Luca') 95 | expect(queryByTestId('input')).toHaveDisplayValue(/Luc/) 96 | 97 | queryByTestId('input').value = 'Piero' 98 | expect(queryByTestId('input')).toHaveDisplayValue('Piero') 99 | }) 100 | 101 | test('it should work with textarea elements', () => { 102 | const {queryByTestId} = render( 103 | '', 104 | ) 105 | 106 | expect(queryByTestId('textarea-example')).toHaveDisplayValue( 107 | 'An example description here.', 108 | ) 109 | expect(queryByTestId('textarea-example')).toHaveDisplayValue(/example/) 110 | 111 | queryByTestId('textarea-example').value = 'Another example' 112 | expect(queryByTestId('textarea-example')).toHaveDisplayValue( 113 | 'Another example', 114 | ) 115 | }) 116 | 117 | test('it should throw if element is not valid', () => { 118 | const {queryByTestId} = render(` 119 |
Banana
120 | 121 | 122 | `) 123 | 124 | let errorMessage 125 | try { 126 | expect(queryByTestId('div')).toHaveDisplayValue('Banana') 127 | } catch (err) { 128 | errorMessage = err.message 129 | } 130 | 131 | expect(errorMessage).toMatchInlineSnapshot( 132 | `.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.`, 133 | ) 134 | 135 | try { 136 | expect(queryByTestId('radio')).toHaveDisplayValue('Something') 137 | } catch (err) { 138 | errorMessage = err.message 139 | } 140 | 141 | expect(errorMessage).toMatchInlineSnapshot( 142 | `.toHaveDisplayValue() currently does not support input[type="radio"], try with another matcher instead.`, 143 | ) 144 | 145 | try { 146 | expect(queryByTestId('checkbox')).toHaveDisplayValue(true) 147 | } catch (err) { 148 | errorMessage = err.message 149 | } 150 | 151 | expect(errorMessage).toMatchInlineSnapshot( 152 | `.toHaveDisplayValue() currently does not support input[type="checkbox"], try with another matcher instead.`, 153 | ) 154 | }) 155 | 156 | test('it should work with numbers', () => { 157 | const {queryByTestId} = render(` 158 | 161 | `) 162 | 163 | expect(queryByTestId('select')).toHaveDisplayValue(1) 164 | }) 165 | -------------------------------------------------------------------------------- /src/__tests__/to-have-errormessage.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | // eslint-disable-next-line max-lines-per-function 4 | describe('.toHaveErrorMessage', () => { 5 | test('resolves for object with correct aria-errormessage reference', () => { 6 | const {queryByTestId} = render(` 7 | 8 | 9 | Invalid time: the time must be between 9:00 AM and 5:00 PM 10 | `) 11 | 12 | const timeInput = queryByTestId('startTime') 13 | 14 | expect(timeInput).toHaveErrorMessage( 15 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 16 | ) 17 | expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match 18 | expect(timeInput).toHaveErrorMessage( 19 | expect.stringContaining('Invalid time'), 20 | ) // to partially match 21 | expect(timeInput).not.toHaveErrorMessage('Pikachu!') 22 | }) 23 | 24 | test('works correctly on implicit invalid element', () => { 25 | const {queryByTestId} = render(` 26 | 27 | 28 | Invalid time: the time must be between 9:00 AM and 5:00 PM 29 | `) 30 | 31 | const timeInput = queryByTestId('startTime') 32 | 33 | expect(timeInput).toHaveErrorMessage( 34 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 35 | ) 36 | expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match 37 | expect(timeInput).toHaveErrorMessage( 38 | expect.stringContaining('Invalid time'), 39 | ) // to partially match 40 | expect(timeInput).not.toHaveErrorMessage('Pikachu!') 41 | }) 42 | 43 | test('rejects for valid object', () => { 44 | const {queryByTestId} = render(` 45 |
The errormessage
46 |
47 |
48 | `) 49 | 50 | expect(queryByTestId('valid')).not.toHaveErrorMessage('The errormessage') 51 | expect(() => { 52 | expect(queryByTestId('valid')).toHaveErrorMessage('The errormessage') 53 | }).toThrowError() 54 | 55 | expect(queryByTestId('explicitly_valid')).not.toHaveErrorMessage( 56 | 'The errormessage', 57 | ) 58 | expect(() => { 59 | expect(queryByTestId('explicitly_valid')).toHaveErrorMessage( 60 | 'The errormessage', 61 | ) 62 | }).toThrowError() 63 | }) 64 | 65 | test('rejects for object with incorrect aria-errormessage reference', () => { 66 | const {queryByTestId} = render(` 67 |
The errormessage
68 |
69 | `) 70 | 71 | expect(queryByTestId('invalid_id')).not.toHaveErrorMessage() 72 | expect(queryByTestId('invalid_id')).toHaveErrorMessage('') 73 | }) 74 | 75 | test('handles invalid element without aria-errormessage', () => { 76 | const {queryByTestId} = render(` 77 |
The errormessage
78 |
79 | `) 80 | 81 | expect(queryByTestId('without')).not.toHaveErrorMessage() 82 | expect(queryByTestId('without')).toHaveErrorMessage('') 83 | }) 84 | 85 | test('handles valid element without aria-errormessage', () => { 86 | const {queryByTestId} = render(` 87 |
The errormessage
88 |
89 | `) 90 | 91 | expect(queryByTestId('without')).not.toHaveErrorMessage() 92 | expect(() => { 93 | expect(queryByTestId('without')).toHaveErrorMessage() 94 | }).toThrowError() 95 | 96 | expect(queryByTestId('without')).not.toHaveErrorMessage('') 97 | expect(() => { 98 | expect(queryByTestId('without')).toHaveErrorMessage('') 99 | }).toThrowError() 100 | }) 101 | 102 | test('handles multiple ids', () => { 103 | const {queryByTestId} = render(` 104 |
First errormessage
105 |
Second errormessage
106 |
Third errormessage
107 |
108 | `) 109 | 110 | expect(queryByTestId('multiple')).toHaveErrorMessage( 111 | 'First errormessage Second errormessage Third errormessage', 112 | ) 113 | expect(queryByTestId('multiple')).toHaveErrorMessage( 114 | /Second errormessage Third/, 115 | ) 116 | expect(queryByTestId('multiple')).toHaveErrorMessage( 117 | expect.stringContaining('Second errormessage Third'), 118 | ) 119 | expect(queryByTestId('multiple')).toHaveErrorMessage( 120 | expect.stringMatching(/Second errormessage Third/), 121 | ) 122 | expect(queryByTestId('multiple')).not.toHaveErrorMessage('Something else') 123 | expect(queryByTestId('multiple')).not.toHaveErrorMessage('First') 124 | }) 125 | 126 | test('handles negative test cases', () => { 127 | const {queryByTestId} = render(` 128 |
The errormessage
129 |
130 | `) 131 | 132 | expect(() => 133 | expect(queryByTestId('other')).toHaveErrorMessage('The errormessage'), 134 | ).toThrowError() 135 | 136 | expect(() => 137 | expect(queryByTestId('target')).toHaveErrorMessage('Something else'), 138 | ).toThrowError() 139 | 140 | expect(() => 141 | expect(queryByTestId('target')).not.toHaveErrorMessage( 142 | 'The errormessage', 143 | ), 144 | ).toThrowError() 145 | }) 146 | 147 | test('normalizes whitespace', () => { 148 | const {queryByTestId} = render(` 149 |
150 | Step 151 | 1 152 | of 153 | 4 154 |
155 |
156 | And 157 | extra 158 | errormessage 159 |
160 |
161 | `) 162 | 163 | expect(queryByTestId('target')).toHaveErrorMessage( 164 | 'Step 1 of 4 And extra errormessage', 165 | ) 166 | }) 167 | 168 | test('can handle multiple levels with content spread across decendants', () => { 169 | const {queryByTestId} = render(` 170 | 171 | Step 172 | 1 173 | of 174 | 4 175 | 176 |
177 | `) 178 | 179 | expect(queryByTestId('target')).toHaveErrorMessage('Step 1 of 4') 180 | }) 181 | 182 | test('handles extra whitespace with multiple ids', () => { 183 | const {queryByTestId} = render(` 184 |
First errormessage
185 |
Second errormessage
186 |
Third errormessage
187 |
190 | `) 191 | 192 | expect(queryByTestId('multiple')).toHaveErrorMessage( 193 | 'First errormessage Second errormessage Third errormessage', 194 | ) 195 | }) 196 | 197 | test('is case-sensitive', () => { 198 | const {queryByTestId} = render(` 199 | Sensitive text 200 |
201 | `) 202 | 203 | expect(queryByTestId('target')).toHaveErrorMessage('Sensitive text') 204 | expect(queryByTestId('target')).not.toHaveErrorMessage('sensitive text') 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /src/__tests__/to-have-focus.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | import document from './helpers/document' 3 | 4 | test('.toHaveFocus', () => { 5 | const {container} = render(` 6 |
7 | 8 | 9 | 10 |
`) 11 | 12 | const focused = container.querySelector('#focused') 13 | const notFocused = container.querySelector('#not-focused') 14 | 15 | document.body.appendChild(container) 16 | focused.focus() 17 | 18 | expect(focused).toHaveFocus() 19 | expect(notFocused).not.toHaveFocus() 20 | 21 | expect(() => expect(focused).not.toHaveFocus()).toThrowError() 22 | expect(() => expect(notFocused).toHaveFocus()).toThrowError() 23 | }) 24 | -------------------------------------------------------------------------------- /src/__tests__/to-have-role.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toHaveRole', () => { 4 | it('matches implicit role', () => { 5 | const {queryByTestId} = render(` 6 |
7 | 8 |
9 | `) 10 | 11 | const continueButton = queryByTestId('continue-button') 12 | 13 | expect(continueButton).not.toHaveRole('listitem') 14 | expect(continueButton).toHaveRole('button') 15 | 16 | expect(() => { 17 | expect(continueButton).toHaveRole('listitem') 18 | }).toThrow(/expected element to have role/i) 19 | expect(() => { 20 | expect(continueButton).not.toHaveRole('button') 21 | }).toThrow(/expected element not to have role/i) 22 | }) 23 | 24 | it('matches explicit role', () => { 25 | const {queryByTestId} = render(` 26 |
27 |
Continue
28 |
29 | `) 30 | 31 | const continueButton = queryByTestId('continue-button') 32 | 33 | expect(continueButton).not.toHaveRole('listitem') 34 | expect(continueButton).toHaveRole('button') 35 | 36 | expect(() => { 37 | expect(continueButton).toHaveRole('listitem') 38 | }).toThrow(/expected element to have role/i) 39 | expect(() => { 40 | expect(continueButton).not.toHaveRole('button') 41 | }).toThrow(/expected element not to have role/i) 42 | }) 43 | 44 | it('matches multiple explicit roles', () => { 45 | const {queryByTestId} = render(` 46 |
47 |
Continue
48 |
49 | `) 50 | 51 | const continueButton = queryByTestId('continue-button') 52 | 53 | expect(continueButton).not.toHaveRole('listitem') 54 | expect(continueButton).toHaveRole('button') 55 | expect(continueButton).toHaveRole('switch') 56 | 57 | expect(() => { 58 | expect(continueButton).toHaveRole('listitem') 59 | }).toThrow(/expected element to have role/i) 60 | expect(() => { 61 | expect(continueButton).not.toHaveRole('button') 62 | }).toThrow(/expected element not to have role/i) 63 | expect(() => { 64 | expect(continueButton).not.toHaveRole('switch') 65 | }).toThrow(/expected element not to have role/i) 66 | }) 67 | 68 | // At this point, we might be testing the details of getImplicitAriaRoles, but 69 | // it's good to have a gut check 70 | it('handles implicit roles with multiple conditions', () => { 71 | const {queryByTestId} = render(` 72 | 76 | `) 77 | 78 | const validLink = queryByTestId('link-valid') 79 | const invalidLink = queryByTestId('link-invalid') 80 | 81 | // valid link has role 'link' 82 | expect(validLink).not.toHaveRole('listitem') 83 | expect(validLink).toHaveRole('link') 84 | 85 | expect(() => { 86 | expect(validLink).toHaveRole('listitem') 87 | }).toThrow(/expected element to have role/i) 88 | expect(() => { 89 | expect(validLink).not.toHaveRole('link') 90 | }).toThrow(/expected element not to have role/i) 91 | 92 | // invalid link has role 'generic' 93 | expect(invalidLink).not.toHaveRole('listitem') 94 | expect(invalidLink).not.toHaveRole('link') 95 | expect(invalidLink).toHaveRole('generic') 96 | 97 | expect(() => { 98 | expect(invalidLink).toHaveRole('listitem') 99 | }).toThrow(/expected element to have role/i) 100 | expect(() => { 101 | expect(invalidLink).toHaveRole('link') 102 | }).toThrow(/expected element to have role/i) 103 | expect(() => { 104 | expect(invalidLink).not.toHaveRole('generic') 105 | }).toThrow(/expected element not to have role/i) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /src/__tests__/to-have-selection.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toHaveSelection', () => { 4 | test.each(['text', 'password', 'textarea'])( 5 | 'handles selection within form elements', 6 | testId => { 7 | const {queryByTestId} = render(` 8 | 9 | 10 | 11 | `) 12 | 13 | queryByTestId(testId).setSelectionRange(5, 13) 14 | expect(queryByTestId(testId)).toHaveSelection('selected') 15 | 16 | queryByTestId(testId).select() 17 | expect(queryByTestId(testId)).toHaveSelection('text selected text') 18 | }, 19 | ) 20 | 21 | test.each(['checkbox', 'radio'])( 22 | 'returns empty string for form elements without text', 23 | testId => { 24 | const {queryByTestId} = render(` 25 | 26 | 27 | `) 28 | 29 | queryByTestId(testId).select() 30 | expect(queryByTestId(testId)).toHaveSelection('') 31 | }, 32 | ) 33 | 34 | test('does not match subset string', () => { 35 | const {queryByTestId} = render(` 36 | 37 | `) 38 | 39 | queryByTestId('text').setSelectionRange(5, 13) 40 | expect(queryByTestId('text')).not.toHaveSelection('select') 41 | expect(queryByTestId('text')).toHaveSelection('selected') 42 | }) 43 | 44 | test('accepts any selection when expected selection is missing', () => { 45 | const {queryByTestId} = render(` 46 | 47 | `) 48 | 49 | expect(queryByTestId('text')).not.toHaveSelection() 50 | 51 | queryByTestId('text').setSelectionRange(5, 13) 52 | 53 | expect(queryByTestId('text')).toHaveSelection() 54 | }) 55 | 56 | test('throws when form element is not selected', () => { 57 | const {queryByTestId} = render(` 58 | 59 | `) 60 | 61 | expect(() => 62 | expect(queryByTestId('text')).toHaveSelection(), 63 | ).toThrowErrorMatchingInlineSnapshot( 64 | ` 65 | expect(element).toHaveSelection(expected) 66 | 67 | Expected the element to have selection: 68 | (any) 69 | Received: 70 | 71 | `, 72 | ) 73 | }) 74 | 75 | test('throws when form element is selected', () => { 76 | const {queryByTestId} = render(` 77 | 78 | `) 79 | queryByTestId('text').setSelectionRange(5, 13) 80 | 81 | expect(() => 82 | expect(queryByTestId('text')).not.toHaveSelection(), 83 | ).toThrowErrorMatchingInlineSnapshot( 84 | ` 85 | expect(element).not.toHaveSelection(expected) 86 | 87 | Expected the element not to have selection: 88 | (any) 89 | Received: 90 | selected 91 | `, 92 | ) 93 | }) 94 | 95 | test('throws when element is not selected', () => { 96 | const {queryByTestId} = render(` 97 |
text
98 | `) 99 | 100 | expect(() => 101 | expect(queryByTestId('text')).toHaveSelection(), 102 | ).toThrowErrorMatchingInlineSnapshot( 103 | ` 104 | expect(element).toHaveSelection(expected) 105 | 106 | Expected the element to have selection: 107 | (any) 108 | Received: 109 | 110 | `, 111 | ) 112 | }) 113 | 114 | test('throws when element selection does not match', () => { 115 | const {queryByTestId} = render(` 116 | 117 | `) 118 | queryByTestId('text').setSelectionRange(0, 4) 119 | 120 | expect(() => 121 | expect(queryByTestId('text')).toHaveSelection('no match'), 122 | ).toThrowErrorMatchingInlineSnapshot( 123 | ` 124 | expect(element).toHaveSelection(no match) 125 | 126 | Expected the element to have selection: 127 | no match 128 | Received: 129 | text 130 | `, 131 | ) 132 | }) 133 | 134 | test('handles selection within text nodes', () => { 135 | const {queryByTestId} = render(` 136 |
137 |
prev
138 |
text selected text
139 |
next
140 |
141 | `) 142 | 143 | const selection = queryByTestId('child').ownerDocument.getSelection() 144 | const range = queryByTestId('child').ownerDocument.createRange() 145 | selection.removeAllRanges() 146 | selection.empty() 147 | selection.addRange(range) 148 | 149 | range.selectNodeContents(queryByTestId('child')) 150 | 151 | expect(queryByTestId('child')).toHaveSelection('selected') 152 | expect(queryByTestId('parent')).toHaveSelection('selected') 153 | 154 | range.selectNodeContents(queryByTestId('parent')) 155 | 156 | expect(queryByTestId('child')).toHaveSelection('selected') 157 | expect(queryByTestId('parent')).toHaveSelection('text selected text') 158 | 159 | range.setStart(queryByTestId('prev'), 0) 160 | range.setEnd(queryByTestId('child').childNodes[0], 3) 161 | 162 | expect(queryByTestId('prev')).toHaveSelection('prev') 163 | expect(queryByTestId('child')).toHaveSelection('sel') 164 | expect(queryByTestId('parent')).toHaveSelection('text sel') 165 | expect(queryByTestId('next')).not.toHaveSelection() 166 | 167 | range.setStart(queryByTestId('child').childNodes[0], 3) 168 | range.setEnd(queryByTestId('next').childNodes[0], 2) 169 | 170 | expect(queryByTestId('child')).toHaveSelection('ected') 171 | expect(queryByTestId('parent')).toHaveSelection('ected text') 172 | expect(queryByTestId('prev')).not.toHaveSelection() 173 | expect(queryByTestId('next')).toHaveSelection('ne') 174 | }) 175 | 176 | test('throws with information when the expected selection is not string', () => { 177 | const {container} = render(`
1
`) 178 | const element = container.firstChild 179 | const range = element.ownerDocument.createRange() 180 | range.selectNodeContents(element) 181 | element.ownerDocument.getSelection().addRange(range) 182 | 183 | expect(() => 184 | expect(element).toHaveSelection(1), 185 | ).toThrowErrorMatchingInlineSnapshot( 186 | `expected selection must be a string or undefined`, 187 | ) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /src/__tests__/to-have-text-content.js: -------------------------------------------------------------------------------- 1 | import {render} from './helpers/test-utils' 2 | 3 | describe('.toHaveTextContent', () => { 4 | test('handles positive test cases', () => { 5 | const {queryByTestId} = render(`2`) 6 | 7 | expect(queryByTestId('count-value')).toHaveTextContent('2') 8 | expect(queryByTestId('count-value')).toHaveTextContent(2) 9 | expect(queryByTestId('count-value')).toHaveTextContent(/2/) 10 | expect(queryByTestId('count-value')).not.toHaveTextContent('21') 11 | }) 12 | 13 | test('handles text nodes', () => { 14 | const {container} = render(`example`) 15 | 16 | expect(container.querySelector('span').firstChild).toHaveTextContent( 17 | 'example', 18 | ) 19 | }) 20 | 21 | test('handles fragments', () => { 22 | const {asFragment} = render(`example`) 23 | 24 | expect(asFragment()).toHaveTextContent('example') 25 | }) 26 | 27 | test('handles negative test cases', () => { 28 | const {queryByTestId} = render(`2`) 29 | 30 | expect(() => 31 | expect(queryByTestId('count-value2')).toHaveTextContent('2'), 32 | ).toThrowError() 33 | 34 | expect(() => 35 | expect(queryByTestId('count-value')).toHaveTextContent('3'), 36 | ).toThrowError() 37 | expect(() => 38 | expect(queryByTestId('count-value')).not.toHaveTextContent('2'), 39 | ).toThrowError() 40 | }) 41 | 42 | test('normalizes whitespace by default', () => { 43 | const {container} = render(` 44 | 45 | Step 46 | 1 47 | of 48 | 4 49 | 50 | `) 51 | 52 | expect(container.querySelector('span')).toHaveTextContent('Step 1 of 4') 53 | }) 54 | 55 | test('allows whitespace normalization to be turned off', () => { 56 | const {container} = render(`  Step 1 of 4`) 57 | 58 | expect(container.querySelector('span')).toHaveTextContent(' Step 1 of 4', { 59 | normalizeWhitespace: false, 60 | }) 61 | }) 62 | 63 | test('can handle multiple levels', () => { 64 | const {container} = render(`Step 1 65 | 66 | of 4`) 67 | 68 | expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4') 69 | }) 70 | 71 | test('can handle multiple levels with content spread across decendants', () => { 72 | const {container} = render(` 73 | 74 | Step 75 | 1 76 | of 77 | 78 | 79 | 4 80 | 81 | `) 82 | 83 | expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4') 84 | }) 85 | 86 | test('does not throw error with empty content', () => { 87 | const {container} = render(``) 88 | expect(container.querySelector('span')).toHaveTextContent('') 89 | }) 90 | 91 | test('is case-sensitive', () => { 92 | const {container} = render('Sensitive text') 93 | 94 | expect(container.querySelector('span')).toHaveTextContent('Sensitive text') 95 | expect(container.querySelector('span')).not.toHaveTextContent( 96 | 'sensitive text', 97 | ) 98 | }) 99 | 100 | test('when matching with empty string and element with content, suggest using toBeEmptyDOMElement instead', () => { 101 | // https://github.com/testing-library/jest-dom/issues/104 102 | const {container} = render('not empty') 103 | 104 | expect(() => 105 | expect(container.querySelector('span')).toHaveTextContent(''), 106 | ).toThrowError(/toBeEmptyDOMElement\(\)/) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | deprecate, 3 | checkHtmlElement, 4 | checkNode, 5 | HtmlElementTypeError, 6 | NodeTypeError, 7 | toSentence, 8 | } from '../utils' 9 | import document from './helpers/document' 10 | 11 | test('deprecate', () => { 12 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}) 13 | const name = 'test' 14 | const replacement = 'test' 15 | const message = `Warning: ${name} has been deprecated and will be removed in future updates.` 16 | 17 | deprecate(name, replacement) 18 | expect(spy).toHaveBeenCalledWith(message, replacement) 19 | 20 | deprecate(name) 21 | expect(spy).toHaveBeenCalledWith(message, undefined) 22 | 23 | spy.mockRestore() 24 | }) 25 | 26 | describe('checkHtmlElement', () => { 27 | let assertionContext 28 | beforeAll(() => { 29 | expect.extend({ 30 | fakeMatcher() { 31 | assertionContext = this 32 | 33 | return {pass: true} 34 | }, 35 | }) 36 | 37 | expect(true).fakeMatcher(true) 38 | }) 39 | it('does not throw an error for correct html element', () => { 40 | expect(() => { 41 | const element = document.createElement('p') 42 | checkHtmlElement(element, () => {}, assertionContext) 43 | }).not.toThrow() 44 | }) 45 | 46 | it('does not throw an error for correct svg element', () => { 47 | expect(() => { 48 | const element = document.createElementNS( 49 | 'http://www.w3.org/2000/svg', 50 | 'rect', 51 | ) 52 | checkHtmlElement(element, () => {}, assertionContext) 53 | }).not.toThrow() 54 | }) 55 | 56 | it('does not throw for body', () => { 57 | expect(() => { 58 | checkHtmlElement(document.body, () => {}, assertionContext) 59 | }).not.toThrow() 60 | }) 61 | 62 | it('throws for undefined', () => { 63 | expect(() => { 64 | checkHtmlElement(undefined, () => {}, assertionContext) 65 | }).toThrow(HtmlElementTypeError) 66 | }) 67 | 68 | it('throws for document', () => { 69 | expect(() => { 70 | checkHtmlElement(document, () => {}, assertionContext) 71 | }).toThrow(HtmlElementTypeError) 72 | }) 73 | 74 | it('throws for function', () => { 75 | expect(() => { 76 | checkHtmlElement( 77 | () => {}, 78 | () => {}, 79 | assertionContext, 80 | ) 81 | }).toThrow(HtmlElementTypeError) 82 | }) 83 | 84 | it('throws for almost element-like objects', () => { 85 | class FakeObject {} 86 | expect(() => { 87 | checkHtmlElement( 88 | { 89 | ownerDocument: { 90 | defaultView: {HTMLElement: FakeObject, SVGElement: FakeObject}, 91 | }, 92 | }, 93 | () => {}, 94 | assertionContext, 95 | ) 96 | }).toThrow(HtmlElementTypeError) 97 | }) 98 | }) 99 | 100 | describe('checkNode', () => { 101 | let assertionContext 102 | beforeAll(() => { 103 | expect.extend({ 104 | fakeMatcher() { 105 | assertionContext = this 106 | 107 | return {pass: true} 108 | }, 109 | }) 110 | 111 | expect(true).fakeMatcher(true) 112 | }) 113 | it('does not throw an error for correct html element', () => { 114 | expect(() => { 115 | const element = document.createElement('p') 116 | checkNode(element, () => {}, assertionContext) 117 | }).not.toThrow() 118 | }) 119 | 120 | it('does not throw an error for correct svg element', () => { 121 | expect(() => { 122 | const element = document.createElementNS( 123 | 'http://www.w3.org/2000/svg', 124 | 'rect', 125 | ) 126 | checkNode(element, () => {}, assertionContext) 127 | }).not.toThrow() 128 | }) 129 | 130 | it('does not throw an error for Document fragments', () => { 131 | expect(() => { 132 | const fragment = document.createDocumentFragment() 133 | checkNode(fragment, () => {}, assertionContext) 134 | }).not.toThrow() 135 | }) 136 | 137 | it('does not throw an error for text nodes', () => { 138 | expect(() => { 139 | const text = document.createTextNode('foo') 140 | checkNode(text, () => {}, assertionContext) 141 | }).not.toThrow() 142 | }) 143 | 144 | it('does not throw for body', () => { 145 | expect(() => { 146 | checkNode(document.body, () => {}, assertionContext) 147 | }).not.toThrow() 148 | }) 149 | 150 | it('throws for undefined', () => { 151 | expect(() => { 152 | checkNode(undefined, () => {}, assertionContext) 153 | }).toThrow(NodeTypeError) 154 | }) 155 | 156 | it('throws for document', () => { 157 | expect(() => { 158 | checkNode(document, () => {}, assertionContext) 159 | }).toThrow(NodeTypeError) 160 | }) 161 | 162 | it('throws for function', () => { 163 | expect(() => { 164 | checkNode( 165 | () => {}, 166 | () => {}, 167 | assertionContext, 168 | ) 169 | }).toThrow(NodeTypeError) 170 | }) 171 | 172 | it('throws for almost element-like objects', () => { 173 | class FakeObject {} 174 | expect(() => { 175 | checkNode( 176 | { 177 | ownerDocument: { 178 | defaultView: {Node: FakeObject, SVGElement: FakeObject}, 179 | }, 180 | }, 181 | () => {}, 182 | assertionContext, 183 | ) 184 | }).toThrow(NodeTypeError) 185 | }) 186 | }) 187 | 188 | describe('toSentence', () => { 189 | it('turns array into string of comma separated list with default last word connector', () => { 190 | expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three') 191 | }) 192 | 193 | it('supports custom word connector', () => { 194 | expect(toSentence(['one', 'two', 'three'], {wordConnector: '; '})).toBe( 195 | 'one; two and three', 196 | ) 197 | }) 198 | 199 | it('supports custom last word connector', () => { 200 | expect( 201 | toSentence(['one', 'two', 'three'], {lastWordConnector: ' or '}), 202 | ).toBe('one, two or three') 203 | }) 204 | 205 | it('turns one element array into string containing first element', () => { 206 | expect(toSentence(['one'])).toBe('one') 207 | }) 208 | 209 | it('turns empty array into empty string', () => { 210 | expect(toSentence([])).toBe('') 211 | }) 212 | }) 213 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as extensions from './matchers' 2 | 3 | expect.extend(extensions) 4 | -------------------------------------------------------------------------------- /src/jest-globals.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import {expect} from '@jest/globals' 4 | import * as extensions from './matchers' 5 | 6 | expect.extend(extensions) 7 | -------------------------------------------------------------------------------- /src/matchers.js: -------------------------------------------------------------------------------- 1 | export {toBeInTheDOM} from './to-be-in-the-dom' 2 | export {toBeInTheDocument} from './to-be-in-the-document' 3 | export {toBeEmpty} from './to-be-empty' 4 | export {toBeEmptyDOMElement} from './to-be-empty-dom-element' 5 | export {toContainElement} from './to-contain-element' 6 | export {toContainHTML} from './to-contain-html' 7 | export {toHaveTextContent} from './to-have-text-content' 8 | export {toHaveAccessibleDescription} from './to-have-accessible-description' 9 | export {toHaveAccessibleErrorMessage} from './to-have-accessible-errormessage' 10 | export {toHaveRole} from './to-have-role' 11 | export {toHaveAccessibleName} from './to-have-accessible-name' 12 | export {toHaveAttribute} from './to-have-attribute' 13 | export {toHaveClass} from './to-have-class' 14 | export {toHaveStyle} from './to-have-style' 15 | export {toHaveFocus} from './to-have-focus' 16 | export {toHaveFormValues} from './to-have-form-values' 17 | export {toBeVisible} from './to-be-visible' 18 | export {toBeDisabled, toBeEnabled} from './to-be-disabled' 19 | export {toBeRequired} from './to-be-required' 20 | export {toBeInvalid, toBeValid} from './to-be-invalid' 21 | export {toHaveValue} from './to-have-value' 22 | export {toHaveDisplayValue} from './to-have-display-value' 23 | export {toBeChecked} from './to-be-checked' 24 | export {toBePartiallyChecked} from './to-be-partially-checked' 25 | export {toHaveDescription} from './to-have-description' 26 | export {toHaveErrorMessage} from './to-have-errormessage' 27 | export {toHaveSelection} from './to-have-selection' 28 | -------------------------------------------------------------------------------- /src/to-be-checked.js: -------------------------------------------------------------------------------- 1 | import {roles} from 'aria-query' 2 | import {checkHtmlElement, toSentence} from './utils' 3 | 4 | export function toBeChecked(element) { 5 | checkHtmlElement(element, toBeChecked, this) 6 | 7 | const isValidInput = () => { 8 | return ( 9 | element.tagName.toLowerCase() === 'input' && 10 | ['checkbox', 'radio'].includes(element.type) 11 | ) 12 | } 13 | 14 | const isValidAriaElement = () => { 15 | return ( 16 | roleSupportsChecked(element.getAttribute('role')) && 17 | ['true', 'false'].includes(element.getAttribute('aria-checked')) 18 | ) 19 | } 20 | 21 | if (!isValidInput() && !isValidAriaElement()) { 22 | return { 23 | pass: false, 24 | message: () => 25 | `only inputs with type="checkbox" or type="radio" or elements with ${supportedRolesSentence()} and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead`, 26 | } 27 | } 28 | 29 | const isChecked = () => { 30 | if (isValidInput()) return element.checked 31 | return element.getAttribute('aria-checked') === 'true' 32 | } 33 | 34 | return { 35 | pass: isChecked(), 36 | message: () => { 37 | const is = isChecked() ? 'is' : 'is not' 38 | return [ 39 | this.utils.matcherHint( 40 | `${this.isNot ? '.not' : ''}.toBeChecked`, 41 | 'element', 42 | '', 43 | ), 44 | '', 45 | `Received element ${is} checked:`, 46 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 47 | ].join('\n') 48 | }, 49 | } 50 | } 51 | 52 | function supportedRolesSentence() { 53 | return toSentence( 54 | supportedRoles().map(role => `role="${role}"`), 55 | {lastWordConnector: ' or '}, 56 | ) 57 | } 58 | 59 | function supportedRoles() { 60 | return roles.keys().filter(roleSupportsChecked) 61 | } 62 | 63 | function roleSupportsChecked(role) { 64 | return roles.get(role)?.props['aria-checked'] !== undefined 65 | } 66 | -------------------------------------------------------------------------------- /src/to-be-disabled.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getTag} from './utils' 2 | 3 | // form elements that support 'disabled' 4 | const FORM_TAGS = [ 5 | 'fieldset', 6 | 'input', 7 | 'select', 8 | 'optgroup', 9 | 'option', 10 | 'button', 11 | 'textarea', 12 | ] 13 | 14 | /* 15 | * According to specification: 16 | * If
is disabled, the form controls that are its descendants, 17 | * except descendants of its first optional element, are disabled 18 | * 19 | * https://html.spec.whatwg.org/multipage/form-elements.html#concept-fieldset-disabled 20 | * 21 | * This method tests whether element is first legend child of fieldset parent 22 | */ 23 | function isFirstLegendChildOfFieldset(element, parent) { 24 | return ( 25 | getTag(element) === 'legend' && 26 | getTag(parent) === 'fieldset' && 27 | element.isSameNode( 28 | Array.from(parent.children).find(child => getTag(child) === 'legend'), 29 | ) 30 | ) 31 | } 32 | 33 | function isElementDisabledByParent(element, parent) { 34 | return ( 35 | isElementDisabled(parent) && !isFirstLegendChildOfFieldset(element, parent) 36 | ) 37 | } 38 | 39 | function isCustomElement(tag) { 40 | return tag.includes('-') 41 | } 42 | 43 | /* 44 | * Only certain form elements and custom elements can actually be disabled: 45 | * https://html.spec.whatwg.org/multipage/semantics-other.html#disabled-elements 46 | */ 47 | function canElementBeDisabled(element) { 48 | const tag = getTag(element) 49 | return FORM_TAGS.includes(tag) || isCustomElement(tag) 50 | } 51 | 52 | function isElementDisabled(element) { 53 | return canElementBeDisabled(element) && element.hasAttribute('disabled') 54 | } 55 | 56 | function isAncestorDisabled(element) { 57 | const parent = element.parentElement 58 | return ( 59 | Boolean(parent) && 60 | (isElementDisabledByParent(element, parent) || isAncestorDisabled(parent)) 61 | ) 62 | } 63 | 64 | function isElementOrAncestorDisabled(element) { 65 | return ( 66 | canElementBeDisabled(element) && 67 | (isElementDisabled(element) || isAncestorDisabled(element)) 68 | ) 69 | } 70 | 71 | export function toBeDisabled(element) { 72 | checkHtmlElement(element, toBeDisabled, this) 73 | 74 | const isDisabled = isElementOrAncestorDisabled(element) 75 | 76 | return { 77 | pass: isDisabled, 78 | message: () => { 79 | const is = isDisabled ? 'is' : 'is not' 80 | return [ 81 | this.utils.matcherHint( 82 | `${this.isNot ? '.not' : ''}.toBeDisabled`, 83 | 'element', 84 | '', 85 | ), 86 | '', 87 | `Received element ${is} disabled:`, 88 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 89 | ].join('\n') 90 | }, 91 | } 92 | } 93 | 94 | export function toBeEnabled(element) { 95 | checkHtmlElement(element, toBeEnabled, this) 96 | 97 | const isEnabled = !isElementOrAncestorDisabled(element) 98 | 99 | return { 100 | pass: isEnabled, 101 | message: () => { 102 | const is = isEnabled ? 'is' : 'is not' 103 | return [ 104 | this.utils.matcherHint( 105 | `${this.isNot ? '.not' : ''}.toBeEnabled`, 106 | 'element', 107 | '', 108 | ), 109 | '', 110 | `Received element ${is} enabled:`, 111 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 112 | ].join('\n') 113 | }, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/to-be-empty-dom-element.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | export function toBeEmptyDOMElement(element) { 4 | checkHtmlElement(element, toBeEmptyDOMElement, this) 5 | 6 | return { 7 | pass: isEmptyElement(element), 8 | message: () => { 9 | return [ 10 | this.utils.matcherHint( 11 | `${this.isNot ? '.not' : ''}.toBeEmptyDOMElement`, 12 | 'element', 13 | '', 14 | ), 15 | '', 16 | 'Received:', 17 | ` ${this.utils.printReceived(element.innerHTML)}`, 18 | ].join('\n') 19 | }, 20 | } 21 | } 22 | 23 | /** 24 | * Identifies if an element doesn't contain child nodes (excluding comments) 25 | * ℹ Node.COMMENT_NODE can't be used because of the following issue 26 | * https://github.com/jsdom/jsdom/issues/2220 27 | * 28 | * @param {*} element an HtmlElement or SVGElement 29 | * @return {*} true if the element only contains comments or none 30 | */ 31 | function isEmptyElement(element){ 32 | const nonCommentChildNodes = [...element.childNodes].filter(node => node.nodeType !== 8); 33 | return nonCommentChildNodes.length === 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/to-be-empty.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, deprecate} from './utils' 2 | 3 | export function toBeEmpty(element) { 4 | deprecate( 5 | 'toBeEmpty', 6 | 'Please use instead toBeEmptyDOMElement for finding empty nodes in the DOM.', 7 | ) 8 | checkHtmlElement(element, toBeEmpty, this) 9 | 10 | return { 11 | pass: element.innerHTML === '', 12 | message: () => { 13 | return [ 14 | this.utils.matcherHint( 15 | `${this.isNot ? '.not' : ''}.toBeEmpty`, 16 | 'element', 17 | '', 18 | ), 19 | '', 20 | 'Received:', 21 | ` ${this.utils.printReceived(element.innerHTML)}`, 22 | ].join('\n') 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/to-be-in-the-document.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | export function toBeInTheDocument(element) { 4 | if (element !== null || !this.isNot) { 5 | checkHtmlElement(element, toBeInTheDocument, this) 6 | } 7 | 8 | const pass = 9 | element === null 10 | ? false 11 | : element.ownerDocument === element.getRootNode({composed: true}) 12 | 13 | const errorFound = () => { 14 | return `expected document not to contain element, found ${this.utils.stringify( 15 | element.cloneNode(true), 16 | )} instead` 17 | } 18 | const errorNotFound = () => { 19 | return `element could not be found in the document` 20 | } 21 | 22 | return { 23 | pass, 24 | message: () => { 25 | return [ 26 | this.utils.matcherHint( 27 | `${this.isNot ? '.not' : ''}.toBeInTheDocument`, 28 | 'element', 29 | '', 30 | ), 31 | '', 32 | // eslint-disable-next-line new-cap 33 | this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()), 34 | ].join('\n') 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/to-be-in-the-dom.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, deprecate} from './utils' 2 | 3 | export function toBeInTheDOM(element, container) { 4 | deprecate( 5 | 'toBeInTheDOM', 6 | 'Please use toBeInTheDocument for searching the entire document and toContainElement for searching a specific container.', 7 | ) 8 | 9 | if (element) { 10 | checkHtmlElement(element, toBeInTheDOM, this) 11 | } 12 | 13 | if (container) { 14 | checkHtmlElement(container, toBeInTheDOM, this) 15 | } 16 | 17 | return { 18 | pass: container ? container.contains(element) : !!element, 19 | message: () => { 20 | return [ 21 | this.utils.matcherHint( 22 | `${this.isNot ? '.not' : ''}.toBeInTheDOM`, 23 | 'element', 24 | '', 25 | ), 26 | '', 27 | 'Received:', 28 | ` ${this.utils.printReceived( 29 | element ? element.cloneNode(false) : element, 30 | )}`, 31 | ].join('\n') 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/to-be-invalid.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getTag} from './utils' 2 | 3 | const FORM_TAGS = ['form', 'input', 'select', 'textarea'] 4 | 5 | function isElementHavingAriaInvalid(element) { 6 | return ( 7 | element.hasAttribute('aria-invalid') && 8 | element.getAttribute('aria-invalid') !== 'false' 9 | ) 10 | } 11 | 12 | function isSupportsValidityMethod(element) { 13 | return FORM_TAGS.includes(getTag(element)) 14 | } 15 | 16 | function isElementInvalid(element) { 17 | const isHaveAriaInvalid = isElementHavingAriaInvalid(element) 18 | if (isSupportsValidityMethod(element)) { 19 | return isHaveAriaInvalid || !element.checkValidity() 20 | } else { 21 | return isHaveAriaInvalid 22 | } 23 | } 24 | 25 | export function toBeInvalid(element) { 26 | checkHtmlElement(element, toBeInvalid, this) 27 | 28 | const isInvalid = isElementInvalid(element) 29 | 30 | return { 31 | pass: isInvalid, 32 | message: () => { 33 | const is = isInvalid ? 'is' : 'is not' 34 | return [ 35 | this.utils.matcherHint( 36 | `${this.isNot ? '.not' : ''}.toBeInvalid`, 37 | 'element', 38 | '', 39 | ), 40 | '', 41 | `Received element ${is} currently invalid:`, 42 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 43 | ].join('\n') 44 | }, 45 | } 46 | } 47 | 48 | export function toBeValid(element) { 49 | checkHtmlElement(element, toBeValid, this) 50 | 51 | const isValid = !isElementInvalid(element) 52 | 53 | return { 54 | pass: isValid, 55 | message: () => { 56 | const is = isValid ? 'is' : 'is not' 57 | return [ 58 | this.utils.matcherHint( 59 | `${this.isNot ? '.not' : ''}.toBeValid`, 60 | 'element', 61 | '', 62 | ), 63 | '', 64 | `Received element ${is} currently valid:`, 65 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 66 | ].join('\n') 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/to-be-partially-checked.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | export function toBePartiallyChecked(element) { 4 | checkHtmlElement(element, toBePartiallyChecked, this) 5 | 6 | const isValidInput = () => { 7 | return ( 8 | element.tagName.toLowerCase() === 'input' && element.type === 'checkbox' 9 | ) 10 | } 11 | 12 | const isValidAriaElement = () => { 13 | return element.getAttribute('role') === 'checkbox' 14 | } 15 | 16 | if (!isValidInput() && !isValidAriaElement()) { 17 | return { 18 | pass: false, 19 | message: () => 20 | 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead', 21 | } 22 | } 23 | 24 | const isPartiallyChecked = () => { 25 | const isAriaMixed = element.getAttribute('aria-checked') === 'mixed' 26 | 27 | if (isValidInput()) { 28 | return element.indeterminate || isAriaMixed 29 | } 30 | 31 | return isAriaMixed 32 | } 33 | 34 | return { 35 | pass: isPartiallyChecked(), 36 | message: () => { 37 | const is = isPartiallyChecked() ? 'is' : 'is not' 38 | return [ 39 | this.utils.matcherHint( 40 | `${this.isNot ? '.not' : ''}.toBePartiallyChecked`, 41 | 'element', 42 | '', 43 | ), 44 | '', 45 | `Received element ${is} partially checked:`, 46 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 47 | ].join('\n') 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/to-be-required.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getTag} from './utils' 2 | 3 | // form elements that support 'required' 4 | const FORM_TAGS = ['select', 'textarea'] 5 | 6 | const ARIA_FORM_TAGS = ['input', 'select', 'textarea'] 7 | 8 | const UNSUPPORTED_INPUT_TYPES = [ 9 | 'color', 10 | 'hidden', 11 | 'range', 12 | 'submit', 13 | 'image', 14 | 'reset', 15 | ] 16 | 17 | const SUPPORTED_ARIA_ROLES = [ 18 | 'checkbox', 19 | 'combobox', 20 | 'gridcell', 21 | 'listbox', 22 | 'radiogroup', 23 | 'spinbutton', 24 | 'textbox', 25 | 'tree', 26 | ] 27 | 28 | function isRequiredOnFormTagsExceptInput(element) { 29 | return FORM_TAGS.includes(getTag(element)) && element.hasAttribute('required') 30 | } 31 | 32 | function isRequiredOnSupportedInput(element) { 33 | return ( 34 | getTag(element) === 'input' && 35 | element.hasAttribute('required') && 36 | ((element.hasAttribute('type') && 37 | !UNSUPPORTED_INPUT_TYPES.includes(element.getAttribute('type'))) || 38 | !element.hasAttribute('type')) 39 | ) 40 | } 41 | 42 | function isElementRequiredByARIA(element) { 43 | return ( 44 | element.hasAttribute('aria-required') && 45 | element.getAttribute('aria-required') === 'true' && 46 | (ARIA_FORM_TAGS.includes(getTag(element)) || 47 | (element.hasAttribute('role') && 48 | SUPPORTED_ARIA_ROLES.includes(element.getAttribute('role')))) 49 | ) 50 | } 51 | 52 | export function toBeRequired(element) { 53 | checkHtmlElement(element, toBeRequired, this) 54 | 55 | const isRequired = 56 | isRequiredOnFormTagsExceptInput(element) || 57 | isRequiredOnSupportedInput(element) || 58 | isElementRequiredByARIA(element) 59 | 60 | return { 61 | pass: isRequired, 62 | message: () => { 63 | const is = isRequired ? 'is' : 'is not' 64 | return [ 65 | this.utils.matcherHint( 66 | `${this.isNot ? '.not' : ''}.toBeRequired`, 67 | 'element', 68 | '', 69 | ), 70 | '', 71 | `Received element ${is} required:`, 72 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 73 | ].join('\n') 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/to-be-visible.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | function isStyleVisible(element) { 4 | const {getComputedStyle} = element.ownerDocument.defaultView 5 | 6 | const {display, visibility, opacity} = getComputedStyle(element) 7 | return ( 8 | display !== 'none' && 9 | visibility !== 'hidden' && 10 | visibility !== 'collapse' && 11 | opacity !== '0' && 12 | opacity !== 0 13 | ) 14 | } 15 | 16 | function isAttributeVisible(element, previousElement) { 17 | let detailsVisibility 18 | 19 | if (previousElement) { 20 | detailsVisibility = 21 | element.nodeName === 'DETAILS' && previousElement.nodeName !== 'SUMMARY' 22 | ? element.hasAttribute('open') 23 | : true 24 | } else { 25 | detailsVisibility = 26 | element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true 27 | } 28 | 29 | return !element.hasAttribute('hidden') && detailsVisibility 30 | } 31 | 32 | function isElementVisible(element, previousElement) { 33 | return ( 34 | isStyleVisible(element) && 35 | isAttributeVisible(element, previousElement) && 36 | (!element.parentElement || isElementVisible(element.parentElement, element)) 37 | ) 38 | } 39 | 40 | export function toBeVisible(element) { 41 | checkHtmlElement(element, toBeVisible, this) 42 | const isInDocument = 43 | element.ownerDocument === element.getRootNode({composed: true}) 44 | const isVisible = isInDocument && isElementVisible(element) 45 | return { 46 | pass: isVisible, 47 | message: () => { 48 | const is = isVisible ? 'is' : 'is not' 49 | return [ 50 | this.utils.matcherHint( 51 | `${this.isNot ? '.not' : ''}.toBeVisible`, 52 | 'element', 53 | '', 54 | ), 55 | '', 56 | `Received element ${is} visible${ 57 | isInDocument ? '' : ' (element is not in the document)' 58 | }:`, 59 | ` ${this.utils.printReceived(element.cloneNode(false))}`, 60 | ].join('\n') 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/to-contain-element.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | export function toContainElement(container, element) { 4 | checkHtmlElement(container, toContainElement, this) 5 | 6 | if (element !== null) { 7 | checkHtmlElement(element, toContainElement, this) 8 | } 9 | 10 | return { 11 | pass: container.contains(element), 12 | message: () => { 13 | return [ 14 | this.utils.matcherHint( 15 | `${this.isNot ? '.not' : ''}.toContainElement`, 16 | 'element', 17 | 'element', 18 | ), 19 | '', 20 | // eslint-disable-next-line new-cap 21 | this.utils.RECEIVED_COLOR(`${this.utils.stringify( 22 | container.cloneNode(false), 23 | )} ${ 24 | this.isNot ? 'contains:' : 'does not contain:' 25 | } ${this.utils.stringify(element ? element.cloneNode(false) : element)} 26 | `), 27 | ].join('\n') 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/to-contain-html.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | function getNormalizedHtml(container, htmlText) { 4 | const div = container.ownerDocument.createElement('div') 5 | div.innerHTML = htmlText 6 | return div.innerHTML 7 | } 8 | 9 | export function toContainHTML(container, htmlText) { 10 | checkHtmlElement(container, toContainHTML, this) 11 | 12 | if (typeof htmlText !== 'string') { 13 | throw new Error(`.toContainHTML() expects a string value, got ${htmlText}`) 14 | } 15 | 16 | return { 17 | pass: container.outerHTML.includes(getNormalizedHtml(container, htmlText)), 18 | message: () => { 19 | return [ 20 | this.utils.matcherHint( 21 | `${this.isNot ? '.not' : ''}.toContainHTML`, 22 | 'element', 23 | '', 24 | ), 25 | 'Expected:', 26 | // eslint-disable-next-line new-cap 27 | ` ${this.utils.EXPECTED_COLOR(htmlText)}`, 28 | 'Received:', 29 | ` ${this.utils.printReceived(container.cloneNode(true))}`, 30 | ].join('\n') 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/to-have-accessible-description.js: -------------------------------------------------------------------------------- 1 | import {computeAccessibleDescription} from 'dom-accessibility-api' 2 | import {checkHtmlElement, getMessage} from './utils' 3 | 4 | export function toHaveAccessibleDescription( 5 | htmlElement, 6 | expectedAccessibleDescription, 7 | ) { 8 | checkHtmlElement(htmlElement, toHaveAccessibleDescription, this) 9 | const actualAccessibleDescription = computeAccessibleDescription(htmlElement) 10 | const missingExpectedValue = arguments.length === 1 11 | 12 | let pass = false 13 | if (missingExpectedValue) { 14 | // When called without an expected value we only want to validate that the element has an 15 | // accessible description, whatever it may be. 16 | pass = actualAccessibleDescription !== '' 17 | } else { 18 | pass = 19 | expectedAccessibleDescription instanceof RegExp 20 | ? expectedAccessibleDescription.test(actualAccessibleDescription) 21 | : this.equals( 22 | actualAccessibleDescription, 23 | expectedAccessibleDescription, 24 | ) 25 | } 26 | 27 | return { 28 | pass, 29 | 30 | message: () => { 31 | const to = this.isNot ? 'not to' : 'to' 32 | return getMessage( 33 | this, 34 | this.utils.matcherHint( 35 | `${this.isNot ? '.not' : ''}.${toHaveAccessibleDescription.name}`, 36 | 'element', 37 | '', 38 | ), 39 | `Expected element ${to} have accessible description`, 40 | expectedAccessibleDescription, 41 | 'Received', 42 | actualAccessibleDescription, 43 | ) 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/to-have-accessible-errormessage.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage, normalize} from './utils' 2 | 3 | const ariaInvalidName = 'aria-invalid' 4 | const validStates = ['false'] 5 | 6 | // See `aria-errormessage` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage 7 | export function toHaveAccessibleErrorMessage( 8 | htmlElement, 9 | expectedAccessibleErrorMessage, 10 | ) { 11 | checkHtmlElement(htmlElement, toHaveAccessibleErrorMessage, this) 12 | const to = this.isNot ? 'not to' : 'to' 13 | const method = this.isNot 14 | ? '.not.toHaveAccessibleErrorMessage' 15 | : '.toHaveAccessibleErrorMessage' 16 | 17 | // Enforce Valid Id 18 | const errormessageId = htmlElement.getAttribute('aria-errormessage') 19 | const errormessageIdInvalid = !!errormessageId && /\s+/.test(errormessageId) 20 | 21 | if (errormessageIdInvalid) { 22 | return { 23 | pass: false, 24 | message: () => { 25 | return getMessage( 26 | this, 27 | this.utils.matcherHint(method, 'element'), 28 | "Expected element's `aria-errormessage` attribute to be empty or a single, valid ID", 29 | '', 30 | 'Received', 31 | `aria-errormessage="${errormessageId}"`, 32 | ) 33 | }, 34 | } 35 | } 36 | 37 | // See `aria-invalid` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-invalid 38 | const ariaInvalidVal = htmlElement.getAttribute(ariaInvalidName) 39 | const fieldValid = 40 | !htmlElement.hasAttribute(ariaInvalidName) || 41 | validStates.includes(ariaInvalidVal) 42 | 43 | // Enforce Valid `aria-invalid` Attribute 44 | if (fieldValid) { 45 | return { 46 | pass: false, 47 | message: () => { 48 | return getMessage( 49 | this, 50 | this.utils.matcherHint(method, 'element'), 51 | 'Expected element to be marked as invalid with attribute', 52 | `${ariaInvalidName}="${String(true)}"`, 53 | 'Received', 54 | htmlElement.hasAttribute('aria-invalid') 55 | ? `${ariaInvalidName}="${htmlElement.getAttribute(ariaInvalidName)}` 56 | : null, 57 | ) 58 | }, 59 | } 60 | } 61 | 62 | const error = normalize( 63 | htmlElement.ownerDocument.getElementById(errormessageId)?.textContent ?? '', 64 | ) 65 | 66 | return { 67 | pass: 68 | expectedAccessibleErrorMessage === undefined 69 | ? Boolean(error) 70 | : expectedAccessibleErrorMessage instanceof RegExp 71 | ? expectedAccessibleErrorMessage.test(error) 72 | : this.equals(error, expectedAccessibleErrorMessage), 73 | 74 | message: () => { 75 | return getMessage( 76 | this, 77 | this.utils.matcherHint(method, 'element'), 78 | `Expected element ${to} have accessible error message`, 79 | expectedAccessibleErrorMessage ?? '', 80 | 'Received', 81 | error, 82 | ) 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/to-have-accessible-name.js: -------------------------------------------------------------------------------- 1 | import {computeAccessibleName} from 'dom-accessibility-api' 2 | import {checkHtmlElement, getMessage} from './utils' 3 | 4 | export function toHaveAccessibleName(htmlElement, expectedAccessibleName) { 5 | checkHtmlElement(htmlElement, toHaveAccessibleName, this) 6 | const actualAccessibleName = computeAccessibleName(htmlElement) 7 | const missingExpectedValue = arguments.length === 1 8 | 9 | let pass = false 10 | if (missingExpectedValue) { 11 | // When called without an expected value we only want to validate that the element has an 12 | // accessible name, whatever it may be. 13 | pass = actualAccessibleName !== '' 14 | } else { 15 | pass = 16 | expectedAccessibleName instanceof RegExp 17 | ? expectedAccessibleName.test(actualAccessibleName) 18 | : this.equals(actualAccessibleName, expectedAccessibleName) 19 | } 20 | 21 | return { 22 | pass, 23 | 24 | message: () => { 25 | const to = this.isNot ? 'not to' : 'to' 26 | return getMessage( 27 | this, 28 | this.utils.matcherHint( 29 | `${this.isNot ? '.not' : ''}.${toHaveAccessibleName.name}`, 30 | 'element', 31 | '', 32 | ), 33 | `Expected element ${to} have accessible name`, 34 | expectedAccessibleName, 35 | 'Received', 36 | actualAccessibleName, 37 | ) 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/to-have-attribute.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage} from './utils' 2 | 3 | function printAttribute(stringify, name, value) { 4 | return value === undefined ? name : `${name}=${stringify(value)}` 5 | } 6 | 7 | function getAttributeComment(stringify, name, value) { 8 | return value === undefined 9 | ? `element.hasAttribute(${stringify(name)})` 10 | : `element.getAttribute(${stringify(name)}) === ${stringify(value)}` 11 | } 12 | 13 | export function toHaveAttribute(htmlElement, name, expectedValue) { 14 | checkHtmlElement(htmlElement, toHaveAttribute, this) 15 | const isExpectedValuePresent = expectedValue !== undefined 16 | const hasAttribute = htmlElement.hasAttribute(name) 17 | const receivedValue = htmlElement.getAttribute(name) 18 | return { 19 | pass: isExpectedValuePresent 20 | ? hasAttribute && this.equals(receivedValue, expectedValue) 21 | : hasAttribute, 22 | message: () => { 23 | const to = this.isNot ? 'not to' : 'to' 24 | const receivedAttribute = hasAttribute 25 | ? printAttribute(this.utils.stringify, name, receivedValue) 26 | : null 27 | const matcher = this.utils.matcherHint( 28 | `${this.isNot ? '.not' : ''}.toHaveAttribute`, 29 | 'element', 30 | this.utils.printExpected(name), 31 | { 32 | secondArgument: isExpectedValuePresent 33 | ? this.utils.printExpected(expectedValue) 34 | : undefined, 35 | comment: getAttributeComment( 36 | this.utils.stringify, 37 | name, 38 | expectedValue, 39 | ), 40 | }, 41 | ) 42 | return getMessage( 43 | this, 44 | matcher, 45 | `Expected the element ${to} have attribute`, 46 | printAttribute(this.utils.stringify, name, expectedValue), 47 | 'Received', 48 | receivedAttribute, 49 | ) 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/to-have-class.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage} from './utils' 2 | 3 | function getExpectedClassNamesAndOptions(params) { 4 | const lastParam = params.pop() 5 | let expectedClassNames, options 6 | 7 | if (typeof lastParam === 'object' && !(lastParam instanceof RegExp)) { 8 | expectedClassNames = params 9 | options = lastParam 10 | } else { 11 | expectedClassNames = params.concat(lastParam) 12 | options = {exact: false} 13 | } 14 | return {expectedClassNames, options} 15 | } 16 | 17 | function splitClassNames(str) { 18 | if (!str) return [] 19 | return str.split(/\s+/).filter(s => s.length > 0) 20 | } 21 | 22 | function isSubset(subset, superset) { 23 | return subset.every(strOrRegexp => 24 | typeof strOrRegexp === 'string' 25 | ? superset.includes(strOrRegexp) 26 | : superset.some(className => strOrRegexp.test(className)), 27 | ) 28 | } 29 | 30 | export function toHaveClass(htmlElement, ...params) { 31 | checkHtmlElement(htmlElement, toHaveClass, this) 32 | const {expectedClassNames, options} = getExpectedClassNamesAndOptions(params) 33 | 34 | const received = splitClassNames(htmlElement.getAttribute('class')) 35 | const expected = expectedClassNames.reduce( 36 | (acc, className) => 37 | acc.concat( 38 | typeof className === 'string' || !className 39 | ? splitClassNames(className) 40 | : className, 41 | ), 42 | [], 43 | ) 44 | 45 | const hasRegExp = expected.some(className => className instanceof RegExp) 46 | if (options.exact && hasRegExp) { 47 | throw new Error('Exact option does not support RegExp expected class names') 48 | } 49 | 50 | if (options.exact) { 51 | return { 52 | pass: isSubset(expected, received) && expected.length === received.length, 53 | message: () => { 54 | const to = this.isNot ? 'not to' : 'to' 55 | return getMessage( 56 | this, 57 | this.utils.matcherHint( 58 | `${this.isNot ? '.not' : ''}.toHaveClass`, 59 | 'element', 60 | this.utils.printExpected(expected.join(' ')), 61 | ), 62 | `Expected the element ${to} have EXACTLY defined classes`, 63 | expected.join(' '), 64 | 'Received', 65 | received.join(' '), 66 | ) 67 | }, 68 | } 69 | } 70 | 71 | return expected.length > 0 72 | ? { 73 | pass: isSubset(expected, received), 74 | message: () => { 75 | const to = this.isNot ? 'not to' : 'to' 76 | return getMessage( 77 | this, 78 | this.utils.matcherHint( 79 | `${this.isNot ? '.not' : ''}.toHaveClass`, 80 | 'element', 81 | this.utils.printExpected(expected.join(' ')), 82 | ), 83 | `Expected the element ${to} have class`, 84 | expected.join(' '), 85 | 'Received', 86 | received.join(' '), 87 | ) 88 | }, 89 | } 90 | : { 91 | pass: this.isNot ? received.length > 0 : false, 92 | message: () => 93 | this.isNot 94 | ? getMessage( 95 | this, 96 | this.utils.matcherHint('.not.toHaveClass', 'element', ''), 97 | 'Expected the element to have classes', 98 | '(none)', 99 | 'Received', 100 | received.join(' '), 101 | ) 102 | : [ 103 | this.utils.matcherHint(`.toHaveClass`, 'element'), 104 | 'At least one expected class must be provided.', 105 | ].join('\n'), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/to-have-description.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage, normalize, deprecate} from './utils' 2 | 3 | // See algoritm: https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_description 4 | export function toHaveDescription(htmlElement, checkWith) { 5 | deprecate( 6 | 'toHaveDescription', 7 | 'Please use toHaveAccessibleDescription.', 8 | ) 9 | 10 | checkHtmlElement(htmlElement, toHaveDescription, this) 11 | 12 | const expectsDescription = checkWith !== undefined 13 | 14 | const descriptionIDRaw = htmlElement.getAttribute('aria-describedby') || '' 15 | const descriptionIDs = descriptionIDRaw.split(/\s+/).filter(Boolean) 16 | let description = '' 17 | if (descriptionIDs.length > 0) { 18 | const document = htmlElement.ownerDocument 19 | const descriptionEls = descriptionIDs 20 | .map(descriptionID => document.getElementById(descriptionID)) 21 | .filter(Boolean) 22 | description = normalize(descriptionEls.map(el => el.textContent).join(' ')) 23 | } 24 | 25 | return { 26 | pass: expectsDescription 27 | ? checkWith instanceof RegExp 28 | ? checkWith.test(description) 29 | : this.equals(description, checkWith) 30 | : Boolean(description), 31 | message: () => { 32 | const to = this.isNot ? 'not to' : 'to' 33 | return getMessage( 34 | this, 35 | this.utils.matcherHint( 36 | `${this.isNot ? '.not' : ''}.toHaveDescription`, 37 | 'element', 38 | '', 39 | ), 40 | `Expected the element ${to} have description`, 41 | this.utils.printExpected(checkWith), 42 | 'Received', 43 | this.utils.printReceived(description), 44 | ) 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/to-have-display-value.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage} from './utils' 2 | 3 | export function toHaveDisplayValue(htmlElement, expectedValue) { 4 | checkHtmlElement(htmlElement, toHaveDisplayValue, this) 5 | const tagName = htmlElement.tagName.toLowerCase() 6 | 7 | if (!['select', 'input', 'textarea'].includes(tagName)) { 8 | throw new Error( 9 | '.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.', 10 | ) 11 | } 12 | 13 | if (tagName === 'input' && ['radio', 'checkbox'].includes(htmlElement.type)) { 14 | throw new Error( 15 | `.toHaveDisplayValue() currently does not support input[type="${htmlElement.type}"], try with another matcher instead.`, 16 | ) 17 | } 18 | 19 | const values = getValues(tagName, htmlElement) 20 | const expectedValues = getExpectedValues(expectedValue) 21 | const numberOfMatchesWithValues = expectedValues.filter(expected => 22 | values.some(value => 23 | expected instanceof RegExp 24 | ? expected.test(value) 25 | : this.equals(value, String(expected)), 26 | ), 27 | ).length 28 | 29 | const matchedWithAllValues = numberOfMatchesWithValues === values.length 30 | const matchedWithAllExpectedValues = 31 | numberOfMatchesWithValues === expectedValues.length 32 | 33 | return { 34 | pass: matchedWithAllValues && matchedWithAllExpectedValues, 35 | message: () => 36 | getMessage( 37 | this, 38 | this.utils.matcherHint( 39 | `${this.isNot ? '.not' : ''}.toHaveDisplayValue`, 40 | 'element', 41 | '', 42 | ), 43 | `Expected element ${this.isNot ? 'not ' : ''}to have display value`, 44 | expectedValue, 45 | 'Received', 46 | values, 47 | ), 48 | } 49 | } 50 | 51 | function getValues(tagName, htmlElement) { 52 | return tagName === 'select' 53 | ? Array.from(htmlElement) 54 | .filter(option => option.selected) 55 | .map(option => option.textContent) 56 | : [htmlElement.value] 57 | } 58 | 59 | function getExpectedValues(expectedValue) { 60 | return expectedValue instanceof Array ? expectedValue : [expectedValue] 61 | } 62 | -------------------------------------------------------------------------------- /src/to-have-errormessage.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement, getMessage, normalize, deprecate} from './utils' 2 | 3 | // See aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage 4 | export function toHaveErrorMessage(htmlElement, checkWith) { 5 | deprecate('toHaveErrorMessage', 'Please use toHaveAccessibleErrorMessage.') 6 | checkHtmlElement(htmlElement, toHaveErrorMessage, this) 7 | 8 | if ( 9 | !htmlElement.hasAttribute('aria-invalid') || 10 | htmlElement.getAttribute('aria-invalid') === 'false' 11 | ) { 12 | const not = this.isNot ? '.not' : '' 13 | 14 | return { 15 | pass: false, 16 | message: () => { 17 | return getMessage( 18 | this, 19 | this.utils.matcherHint(`${not}.toHaveErrorMessage`, 'element', ''), 20 | `Expected the element to have invalid state indicated by`, 21 | 'aria-invalid="true"', 22 | 'Received', 23 | htmlElement.hasAttribute('aria-invalid') 24 | ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"` 25 | : this.utils.printReceived(''), 26 | ) 27 | }, 28 | } 29 | } 30 | 31 | const expectsErrorMessage = checkWith !== undefined 32 | 33 | const errormessageIDRaw = htmlElement.getAttribute('aria-errormessage') || '' 34 | const errormessageIDs = errormessageIDRaw.split(/\s+/).filter(Boolean) 35 | 36 | let errormessage = '' 37 | if (errormessageIDs.length > 0) { 38 | const document = htmlElement.ownerDocument 39 | 40 | const errormessageEls = errormessageIDs 41 | .map(errormessageID => document.getElementById(errormessageID)) 42 | .filter(Boolean) 43 | 44 | errormessage = normalize( 45 | errormessageEls.map(el => el.textContent).join(' '), 46 | ) 47 | } 48 | 49 | return { 50 | pass: expectsErrorMessage 51 | ? checkWith instanceof RegExp 52 | ? checkWith.test(errormessage) 53 | : this.equals(errormessage, checkWith) 54 | : Boolean(errormessage), 55 | message: () => { 56 | const to = this.isNot ? 'not to' : 'to' 57 | return getMessage( 58 | this, 59 | this.utils.matcherHint( 60 | `${this.isNot ? '.not' : ''}.toHaveErrorMessage`, 61 | 'element', 62 | '', 63 | ), 64 | `Expected the element ${to} have error message`, 65 | this.utils.printExpected(checkWith), 66 | 'Received', 67 | this.utils.printReceived(errormessage), 68 | ) 69 | }, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/to-have-focus.js: -------------------------------------------------------------------------------- 1 | import {checkHtmlElement} from './utils' 2 | 3 | export function toHaveFocus(element) { 4 | checkHtmlElement(element, toHaveFocus, this) 5 | 6 | return { 7 | pass: element.ownerDocument.activeElement === element, 8 | message: () => { 9 | return [ 10 | this.utils.matcherHint( 11 | `${this.isNot ? '.not' : ''}.toHaveFocus`, 12 | 'element', 13 | '', 14 | ), 15 | '', 16 | ...(this.isNot 17 | ? [ 18 | 'Received element is focused:', 19 | ` ${this.utils.printReceived(element)}`, 20 | ] 21 | : [ 22 | 'Expected element with focus:', 23 | ` ${this.utils.printExpected(element)}`, 24 | 'Received element with focus:', 25 | ` ${this.utils.printReceived( 26 | element.ownerDocument.activeElement, 27 | )}`, 28 | ]), 29 | ].join('\n') 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/to-have-form-values.js: -------------------------------------------------------------------------------- 1 | import isEqualWith from 'lodash/isEqualWith.js' 2 | import escape from 'css.escape' 3 | import { 4 | checkHtmlElement, 5 | getSingleElementValue, 6 | compareArraysAsSet, 7 | } from './utils' 8 | 9 | // Returns the combined value of several elements that have the same name 10 | // e.g. radio buttons or groups of checkboxes 11 | function getMultiElementValue(elements) { 12 | const types = [...new Set(elements.map(element => element.type))] 13 | if (types.length !== 1) { 14 | throw new Error( 15 | 'Multiple form elements with the same name must be of the same type', 16 | ) 17 | } 18 | switch (types[0]) { 19 | case 'radio': { 20 | const theChosenOne = elements.find(radio => radio.checked) 21 | return theChosenOne ? theChosenOne.value : undefined 22 | } 23 | case 'checkbox': 24 | return elements 25 | .filter(checkbox => checkbox.checked) 26 | .map(checkbox => checkbox.value) 27 | default: 28 | // NOTE: Not even sure this is a valid use case, but just in case... 29 | return elements.map(element => element.value) 30 | } 31 | } 32 | 33 | function getFormValue(container, name) { 34 | const elements = [...container.querySelectorAll(`[name="${escape(name)}"]`)] 35 | /* istanbul ignore if */ 36 | if (elements.length === 0) { 37 | return undefined // shouldn't happen, but just in case 38 | } 39 | switch (elements.length) { 40 | case 1: 41 | return getSingleElementValue(elements[0]) 42 | default: 43 | return getMultiElementValue(elements) 44 | } 45 | } 46 | 47 | // Strips the `[]` suffix off a form value name 48 | function getPureName(name) { 49 | return /\[\]$/.test(name) ? name.slice(0, -2) : name 50 | } 51 | 52 | function getAllFormValues(container) { 53 | const names = Array.from(container.elements).map(element => element.name) 54 | return names.reduce( 55 | (obj, name) => ({ 56 | ...obj, 57 | [getPureName(name)]: getFormValue(container, name), 58 | }), 59 | {}, 60 | ) 61 | } 62 | 63 | export function toHaveFormValues(formElement, expectedValues) { 64 | checkHtmlElement(formElement, toHaveFormValues, this) 65 | if (!formElement.elements) { 66 | // TODO: Change condition to use instanceof against the appropriate element classes instead 67 | throw new Error('toHaveFormValues must be called on a form or a fieldset') 68 | } 69 | const formValues = getAllFormValues(formElement) 70 | return { 71 | pass: Object.entries(expectedValues).every(([name, expectedValue]) => 72 | isEqualWith(formValues[name], expectedValue, compareArraysAsSet), 73 | ), 74 | message: () => { 75 | const to = this.isNot ? 'not to' : 'to' 76 | const matcher = `${this.isNot ? '.not' : ''}.toHaveFormValues` 77 | const commonKeyValues = Object.keys(formValues) 78 | .filter(key => expectedValues.hasOwnProperty(key)) 79 | .reduce((obj, key) => ({...obj, [key]: formValues[key]}), {}) 80 | return [ 81 | this.utils.matcherHint(matcher, 'element', ''), 82 | `Expected the element ${to} have form values`, 83 | this.utils.diff(expectedValues, commonKeyValues), 84 | ].join('\n\n') 85 | }, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/to-have-role.js: -------------------------------------------------------------------------------- 1 | import {elementRoles} from 'aria-query' 2 | import {checkHtmlElement, getMessage} from './utils' 3 | 4 | const elementRoleList = buildElementRoleList(elementRoles) 5 | 6 | export function toHaveRole(htmlElement, expectedRole) { 7 | checkHtmlElement(htmlElement, toHaveRole, this) 8 | 9 | const actualRoles = getExplicitOrImplicitRoles(htmlElement) 10 | const pass = actualRoles.some(el => el === expectedRole) 11 | 12 | return { 13 | pass, 14 | 15 | message: () => { 16 | const to = this.isNot ? 'not to' : 'to' 17 | return getMessage( 18 | this, 19 | this.utils.matcherHint( 20 | `${this.isNot ? '.not' : ''}.${toHaveRole.name}`, 21 | 'element', 22 | '', 23 | ), 24 | `Expected element ${to} have role`, 25 | expectedRole, 26 | 'Received', 27 | actualRoles.join(', '), 28 | ) 29 | }, 30 | } 31 | } 32 | 33 | function getExplicitOrImplicitRoles(htmlElement) { 34 | const hasExplicitRole = htmlElement.hasAttribute('role') 35 | 36 | if (hasExplicitRole) { 37 | const roleValue = htmlElement.getAttribute('role') 38 | 39 | // Handle fallback roles, such as role="switch button" 40 | // testing-library gates this behind the `queryFallbacks` flag; it is 41 | // unclear why, but it makes sense to support this pattern out of the box 42 | // https://testing-library.com/docs/queries/byrole/#queryfallbacks 43 | return roleValue.split(' ').filter(Boolean) 44 | } 45 | 46 | const implicitRoles = getImplicitAriaRoles(htmlElement) 47 | 48 | return implicitRoles 49 | } 50 | 51 | function getImplicitAriaRoles(currentNode) { 52 | for (const {match, roles} of elementRoleList) { 53 | if (match(currentNode)) { 54 | return [...roles] 55 | } 56 | } 57 | 58 | /* istanbul ignore next */ 59 | return [] // this does not get reached in practice, since elements have at least a 'generic' role 60 | } 61 | 62 | /** 63 | * Transform the roles map (with required attributes and constraints) to a list 64 | * of roles. Each item in the list has functions to match an element against it. 65 | * 66 | * Essentially copied over from [dom-testing-library's 67 | * helpers](https://github.com/testing-library/dom-testing-library/blob/bd04cf95a1ed85a2238f7dfc1a77d5d16b4f59dc/src/role-helpers.js#L80) 68 | * 69 | * TODO: If we are truly just copying over stuff, would it make sense to move 70 | * this to a separate package? 71 | * 72 | * TODO: This technique relies on CSS selectors; are those consistently 73 | * available in all jest-dom environments? Why do other matchers in this package 74 | * not use them like this? 75 | */ 76 | function buildElementRoleList(elementRolesMap) { 77 | function makeElementSelector({name, attributes}) { 78 | return `${name}${attributes 79 | .map(({name: attributeName, value, constraints = []}) => { 80 | const shouldNotExist = constraints.indexOf('undefined') !== -1 81 | if (shouldNotExist) { 82 | return `:not([${attributeName}])` 83 | } else if (value) { 84 | return `[${attributeName}="${value}"]` 85 | } else { 86 | return `[${attributeName}]` 87 | } 88 | }) 89 | .join('')}` 90 | } 91 | 92 | function getSelectorSpecificity({attributes = []}) { 93 | return attributes.length 94 | } 95 | 96 | function bySelectorSpecificity( 97 | {specificity: leftSpecificity}, 98 | {specificity: rightSpecificity}, 99 | ) { 100 | return rightSpecificity - leftSpecificity 101 | } 102 | 103 | function match(element) { 104 | let {attributes = []} = element 105 | 106 | // https://github.com/testing-library/dom-testing-library/issues/814 107 | const typeTextIndex = attributes.findIndex( 108 | attribute => 109 | attribute.value && 110 | attribute.name === 'type' && 111 | attribute.value === 'text', 112 | ) 113 | 114 | if (typeTextIndex >= 0) { 115 | // not using splice to not mutate the attributes array 116 | attributes = [ 117 | ...attributes.slice(0, typeTextIndex), 118 | ...attributes.slice(typeTextIndex + 1), 119 | ] 120 | } 121 | 122 | const selector = makeElementSelector({...element, attributes}) 123 | 124 | return node => { 125 | if (typeTextIndex >= 0 && node.type !== 'text') { 126 | return false 127 | } 128 | 129 | return node.matches(selector) 130 | } 131 | } 132 | 133 | let result = [] 134 | 135 | for (const [element, roles] of elementRolesMap.entries()) { 136 | result = [ 137 | ...result, 138 | { 139 | match: match(element), 140 | roles: Array.from(roles), 141 | specificity: getSelectorSpecificity(element), 142 | }, 143 | ] 144 | } 145 | 146 | return result.sort(bySelectorSpecificity) 147 | } 148 | -------------------------------------------------------------------------------- /src/to-have-selection.js: -------------------------------------------------------------------------------- 1 | import isEqualWith from 'lodash/isEqualWith.js' 2 | import {checkHtmlElement, compareArraysAsSet, getMessage} from './utils' 3 | 4 | /** 5 | * Returns the selection from the element. 6 | * 7 | * @param element {HTMLElement} The element to get the selection from. 8 | * @returns {String} The selection. 9 | */ 10 | function getSelection(element) { 11 | const selection = element.ownerDocument.getSelection() 12 | 13 | if (['input', 'textarea'].includes(element.tagName.toLowerCase())) { 14 | if (['radio', 'checkbox'].includes(element.type)) return '' 15 | return element.value 16 | .toString() 17 | .substring(element.selectionStart, element.selectionEnd) 18 | } 19 | 20 | if (selection.anchorNode === null || selection.focusNode === null) { 21 | // No selection 22 | return '' 23 | } 24 | 25 | const originalRange = selection.getRangeAt(0) 26 | const temporaryRange = element.ownerDocument.createRange() 27 | 28 | if (selection.containsNode(element, false)) { 29 | // Whole element is inside selection 30 | temporaryRange.selectNodeContents(element) 31 | selection.removeAllRanges() 32 | selection.addRange(temporaryRange) 33 | } else if ( 34 | element.contains(selection.anchorNode) && 35 | element.contains(selection.focusNode) 36 | ) { 37 | // Element contains selection, nothing to do 38 | } else { 39 | // Element is partially selected 40 | const selectionStartsWithinElement = 41 | element === originalRange.startContainer || 42 | element.contains(originalRange.startContainer) 43 | const selectionEndsWithinElement = 44 | element === originalRange.endContainer || 45 | element.contains(originalRange.endContainer) 46 | selection.removeAllRanges() 47 | 48 | if (selectionStartsWithinElement || selectionEndsWithinElement) { 49 | temporaryRange.selectNodeContents(element) 50 | 51 | if (selectionStartsWithinElement) { 52 | temporaryRange.setStart( 53 | originalRange.startContainer, 54 | originalRange.startOffset, 55 | ) 56 | } 57 | if (selectionEndsWithinElement) { 58 | temporaryRange.setEnd( 59 | originalRange.endContainer, 60 | originalRange.endOffset, 61 | ) 62 | } 63 | 64 | selection.addRange(temporaryRange) 65 | } 66 | } 67 | 68 | const result = selection.toString() 69 | 70 | selection.removeAllRanges() 71 | selection.addRange(originalRange) 72 | 73 | return result 74 | } 75 | 76 | /** 77 | * Checks if the element has the string selected. 78 | * 79 | * @param htmlElement {HTMLElement} The html element to check the selection for. 80 | * @param expectedSelection {String} The selection as a string. 81 | */ 82 | export function toHaveSelection(htmlElement, expectedSelection) { 83 | checkHtmlElement(htmlElement, toHaveSelection, this) 84 | 85 | const expectsSelection = expectedSelection !== undefined 86 | 87 | if (expectsSelection && typeof expectedSelection !== 'string') { 88 | throw new Error(`expected selection must be a string or undefined`) 89 | } 90 | 91 | const receivedSelection = getSelection(htmlElement) 92 | 93 | return { 94 | pass: expectsSelection 95 | ? isEqualWith(receivedSelection, expectedSelection, compareArraysAsSet) 96 | : Boolean(receivedSelection), 97 | message: () => { 98 | const to = this.isNot ? 'not to' : 'to' 99 | const matcher = this.utils.matcherHint( 100 | `${this.isNot ? '.not' : ''}.toHaveSelection`, 101 | 'element', 102 | expectedSelection, 103 | ) 104 | return getMessage( 105 | this, 106 | matcher, 107 | `Expected the element ${to} have selection`, 108 | expectsSelection ? expectedSelection : '(any)', 109 | 'Received', 110 | receivedSelection, 111 | ) 112 | }, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/to-have-style.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import {checkHtmlElement, parseCSS} from './utils' 3 | 4 | function getStyleDeclaration(document, css) { 5 | const styles = {} 6 | 7 | // The next block is necessary to normalize colors 8 | const copy = document.createElement('div') 9 | Object.keys(css).forEach(property => { 10 | copy.style[property] = css[property] 11 | styles[property] = copy.style[property] 12 | }) 13 | 14 | return styles 15 | } 16 | 17 | function isSubset(styles, computedStyle) { 18 | return ( 19 | !!Object.keys(styles).length && 20 | Object.entries(styles).every(([prop, value]) => { 21 | const isCustomProperty = prop.startsWith('--') 22 | const spellingVariants = [prop] 23 | if (!isCustomProperty) spellingVariants.push(prop.toLowerCase()) 24 | 25 | return spellingVariants.some( 26 | name => 27 | computedStyle[name] === value || 28 | computedStyle.getPropertyValue(name) === value, 29 | ) 30 | }) 31 | ) 32 | } 33 | 34 | function printoutStyles(styles) { 35 | return Object.keys(styles) 36 | .sort() 37 | .map(prop => `${prop}: ${styles[prop]};`) 38 | .join('\n') 39 | } 40 | 41 | // Highlights only style rules that were expected but were not found in the 42 | // received computed styles 43 | function expectedDiff(diffFn, expected, computedStyles) { 44 | const received = Array.from(computedStyles) 45 | .filter(prop => expected[prop] !== undefined) 46 | .reduce( 47 | (obj, prop) => 48 | Object.assign(obj, {[prop]: computedStyles.getPropertyValue(prop)}), 49 | {}, 50 | ) 51 | const diffOutput = diffFn(printoutStyles(expected), printoutStyles(received)) 52 | // Remove the "+ Received" annotation because this is a one-way diff 53 | return diffOutput.replace(`${chalk.red('+ Received')}\n`, '') 54 | } 55 | 56 | export function toHaveStyle(htmlElement, css) { 57 | checkHtmlElement(htmlElement, toHaveStyle, this) 58 | const parsedCSS = 59 | typeof css === 'object' ? css : parseCSS(css, toHaveStyle, this) 60 | const {getComputedStyle} = htmlElement.ownerDocument.defaultView 61 | 62 | const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS) 63 | const received = getComputedStyle(htmlElement) 64 | 65 | return { 66 | pass: isSubset(expected, received), 67 | message: () => { 68 | const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle` 69 | return [ 70 | this.utils.matcherHint(matcher, 'element', ''), 71 | expectedDiff(this.utils.diff, expected, received), 72 | ].join('\n\n') 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/to-have-text-content.js: -------------------------------------------------------------------------------- 1 | import {getMessage, checkNode, matches, normalize} from './utils' 2 | 3 | export function toHaveTextContent( 4 | node, 5 | checkWith, 6 | options = {normalizeWhitespace: true}, 7 | ) { 8 | checkNode(node, toHaveTextContent, this) 9 | 10 | const textContent = options.normalizeWhitespace 11 | ? normalize(node.textContent) 12 | : node.textContent.replace(/\u00a0/g, ' ') // Replace   with normal spaces 13 | 14 | const checkingWithEmptyString = textContent !== '' && checkWith === '' 15 | 16 | return { 17 | pass: !checkingWithEmptyString && matches(textContent, checkWith), 18 | message: () => { 19 | const to = this.isNot ? 'not to' : 'to' 20 | return getMessage( 21 | this, 22 | this.utils.matcherHint( 23 | `${this.isNot ? '.not' : ''}.toHaveTextContent`, 24 | 'element', 25 | '', 26 | ), 27 | checkingWithEmptyString 28 | ? `Checking with empty string will always match, use .toBeEmptyDOMElement() instead` 29 | : `Expected element ${to} have text content`, 30 | checkWith, 31 | 'Received', 32 | textContent, 33 | ) 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/to-have-value.js: -------------------------------------------------------------------------------- 1 | import isEqualWith from 'lodash/isEqualWith.js' 2 | import { 3 | checkHtmlElement, 4 | getMessage, 5 | getSingleElementValue, 6 | compareArraysAsSet, 7 | } from './utils' 8 | 9 | export function toHaveValue(htmlElement, expectedValue) { 10 | checkHtmlElement(htmlElement, toHaveValue, this) 11 | 12 | if ( 13 | htmlElement.tagName.toLowerCase() === 'input' && 14 | ['checkbox', 'radio'].includes(htmlElement.type) 15 | ) { 16 | throw new Error( 17 | 'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toBeChecked() for type=checkbox or .toHaveFormValues() instead', 18 | ) 19 | } 20 | 21 | const receivedValue = getSingleElementValue(htmlElement) 22 | const expectsValue = expectedValue !== undefined 23 | 24 | let expectedTypedValue = expectedValue 25 | let receivedTypedValue = receivedValue 26 | if (expectedValue == receivedValue && expectedValue !== receivedValue) { 27 | expectedTypedValue = `${expectedValue} (${typeof expectedValue})` 28 | receivedTypedValue = `${receivedValue} (${typeof receivedValue})` 29 | } 30 | 31 | return { 32 | pass: expectsValue 33 | ? isEqualWith(receivedValue, expectedValue, compareArraysAsSet) 34 | : Boolean(receivedValue), 35 | message: () => { 36 | const to = this.isNot ? 'not to' : 'to' 37 | const matcher = this.utils.matcherHint( 38 | `${this.isNot ? '.not' : ''}.toHaveValue`, 39 | 'element', 40 | expectedValue, 41 | ) 42 | return getMessage( 43 | this, 44 | matcher, 45 | `Expected the element ${to} have value`, 46 | expectsValue ? expectedTypedValue : '(any)', 47 | 'Received', 48 | receivedTypedValue, 49 | ) 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import redent from 'redent' 2 | import {parse} from '@adobe/css-tools' 3 | 4 | class GenericTypeError extends Error { 5 | constructor(expectedString, received, matcherFn, context) { 6 | super() 7 | 8 | /* istanbul ignore next */ 9 | if (Error.captureStackTrace) { 10 | Error.captureStackTrace(this, matcherFn) 11 | } 12 | let withType = '' 13 | try { 14 | withType = context.utils.printWithType( 15 | 'Received', 16 | received, 17 | context.utils.printReceived, 18 | ) 19 | } catch (e) { 20 | // Can throw for Document: 21 | // https://github.com/jsdom/jsdom/issues/2304 22 | } 23 | this.message = [ 24 | context.utils.matcherHint( 25 | `${context.isNot ? '.not' : ''}.${matcherFn.name}`, 26 | 'received', 27 | '', 28 | ), 29 | '', 30 | // eslint-disable-next-line new-cap 31 | `${context.utils.RECEIVED_COLOR( 32 | 'received', 33 | )} value must ${expectedString}.`, 34 | withType, 35 | ].join('\n') 36 | } 37 | } 38 | 39 | class HtmlElementTypeError extends GenericTypeError { 40 | constructor(...args) { 41 | super('be an HTMLElement or an SVGElement', ...args) 42 | } 43 | } 44 | 45 | class NodeTypeError extends GenericTypeError { 46 | constructor(...args) { 47 | super('be a Node', ...args) 48 | } 49 | } 50 | 51 | function checkHasWindow(htmlElement, ErrorClass, ...args) { 52 | if ( 53 | !htmlElement || 54 | !htmlElement.ownerDocument || 55 | !htmlElement.ownerDocument.defaultView 56 | ) { 57 | throw new ErrorClass(htmlElement, ...args) 58 | } 59 | } 60 | 61 | function checkNode(node, ...args) { 62 | checkHasWindow(node, NodeTypeError, ...args) 63 | const window = node.ownerDocument.defaultView 64 | 65 | if (!(node instanceof window.Node)) { 66 | throw new NodeTypeError(node, ...args) 67 | } 68 | } 69 | 70 | function checkHtmlElement(htmlElement, ...args) { 71 | checkHasWindow(htmlElement, HtmlElementTypeError, ...args) 72 | const window = htmlElement.ownerDocument.defaultView 73 | 74 | if ( 75 | !(htmlElement instanceof window.HTMLElement) && 76 | !(htmlElement instanceof window.SVGElement) 77 | ) { 78 | throw new HtmlElementTypeError(htmlElement, ...args) 79 | } 80 | } 81 | 82 | class InvalidCSSError extends Error { 83 | constructor(received, matcherFn, context) { 84 | super() 85 | 86 | /* istanbul ignore next */ 87 | if (Error.captureStackTrace) { 88 | Error.captureStackTrace(this, matcherFn) 89 | } 90 | this.message = [ 91 | received.message, 92 | '', 93 | // eslint-disable-next-line new-cap 94 | context.utils.RECEIVED_COLOR(`Failing css:`), 95 | // eslint-disable-next-line new-cap 96 | context.utils.RECEIVED_COLOR(`${received.css}`), 97 | ].join('\n') 98 | } 99 | } 100 | 101 | function parseCSS(css, ...args) { 102 | const ast = parse(`selector { ${css} }`, {silent: true}).stylesheet 103 | 104 | if (ast.parsingErrors && ast.parsingErrors.length > 0) { 105 | const {reason, line} = ast.parsingErrors[0] 106 | 107 | throw new InvalidCSSError( 108 | { 109 | css, 110 | message: `Syntax error parsing expected css: ${reason} on line: ${line}`, 111 | }, 112 | ...args, 113 | ) 114 | } 115 | 116 | const parsedRules = ast.rules[0].declarations 117 | .filter(d => d.type === 'declaration') 118 | .reduce( 119 | (obj, {property, value}) => Object.assign(obj, {[property]: value}), 120 | {}, 121 | ) 122 | return parsedRules 123 | } 124 | 125 | function display(context, value) { 126 | return typeof value === 'string' ? value : context.utils.stringify(value) 127 | } 128 | 129 | function getMessage( 130 | context, 131 | matcher, 132 | expectedLabel, 133 | expectedValue, 134 | receivedLabel, 135 | receivedValue, 136 | ) { 137 | return [ 138 | `${matcher}\n`, 139 | // eslint-disable-next-line new-cap 140 | `${expectedLabel}:\n${context.utils.EXPECTED_COLOR( 141 | redent(display(context, expectedValue), 2), 142 | )}`, 143 | // eslint-disable-next-line new-cap 144 | `${receivedLabel}:\n${context.utils.RECEIVED_COLOR( 145 | redent(display(context, receivedValue), 2), 146 | )}`, 147 | ].join('\n') 148 | } 149 | 150 | function matches(textToMatch, matcher) { 151 | if (matcher instanceof RegExp) { 152 | return matcher.test(textToMatch) 153 | } else { 154 | return textToMatch.includes(String(matcher)) 155 | } 156 | } 157 | 158 | function deprecate(name, replacementText) { 159 | // Notify user that they are using deprecated functionality. 160 | // eslint-disable-next-line no-console 161 | console.warn( 162 | `Warning: ${name} has been deprecated and will be removed in future updates.`, 163 | replacementText, 164 | ) 165 | } 166 | 167 | function normalize(text) { 168 | return text.replace(/\s+/g, ' ').trim() 169 | } 170 | 171 | function getTag(element) { 172 | return element.tagName && element.tagName.toLowerCase() 173 | } 174 | 175 | function getSelectValue({multiple, options}) { 176 | const selectedOptions = [...options].filter(option => option.selected) 177 | 178 | if (multiple) { 179 | return [...selectedOptions].map(opt => opt.value) 180 | } 181 | /* istanbul ignore if */ 182 | if (selectedOptions.length === 0) { 183 | return undefined // Couldn't make this happen, but just in case 184 | } 185 | return selectedOptions[0].value 186 | } 187 | 188 | function getInputValue(inputElement) { 189 | switch (inputElement.type) { 190 | case 'number': 191 | return inputElement.value === '' ? null : Number(inputElement.value) 192 | case 'checkbox': 193 | return inputElement.checked 194 | default: 195 | return inputElement.value 196 | } 197 | } 198 | 199 | const rolesSupportingValues = ['meter', 'progressbar', 'slider', 'spinbutton'] 200 | function getAccessibleValue(element) { 201 | if (!rolesSupportingValues.includes(element.getAttribute('role'))) { 202 | return undefined 203 | } 204 | return Number(element.getAttribute('aria-valuenow')) 205 | } 206 | 207 | function getSingleElementValue(element) { 208 | /* istanbul ignore if */ 209 | if (!element) { 210 | return undefined 211 | } 212 | 213 | switch (element.tagName.toLowerCase()) { 214 | case 'input': 215 | return getInputValue(element) 216 | case 'select': 217 | return getSelectValue(element) 218 | default: { 219 | return element.value ?? getAccessibleValue(element) 220 | } 221 | } 222 | } 223 | 224 | function toSentence( 225 | array, 226 | {wordConnector = ', ', lastWordConnector = ' and '} = {}, 227 | ) { 228 | return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join( 229 | array.length > 1 ? lastWordConnector : '', 230 | ) 231 | } 232 | 233 | function compareArraysAsSet(arr1, arr2) { 234 | if (Array.isArray(arr1) && Array.isArray(arr2)) { 235 | return [...new Set(arr1)].every(v => new Set(arr2).has(v)) 236 | } 237 | return undefined 238 | } 239 | 240 | export { 241 | HtmlElementTypeError, 242 | NodeTypeError, 243 | checkHtmlElement, 244 | checkNode, 245 | parseCSS, 246 | deprecate, 247 | getMessage, 248 | matches, 249 | normalize, 250 | getTag, 251 | getSingleElementValue, 252 | toSentence, 253 | compareArraysAsSet, 254 | } 255 | -------------------------------------------------------------------------------- /src/vitest.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import {expect} from 'vitest' 4 | import * as extensions from './matchers' 5 | 6 | expect.extend(extensions) 7 | -------------------------------------------------------------------------------- /tests/jest.config.dom.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('kcd-scripts/jest') 3 | 4 | module.exports = { 5 | rootDir: path.resolve(__dirname, '..'), 6 | displayName: 'jsdom', 7 | testEnvironment: 'dom', 8 | ...config, 9 | } 10 | -------------------------------------------------------------------------------- /tests/jest.config.node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('kcd-scripts/jest') 3 | 4 | module.exports = { 5 | rootDir: path.resolve(__dirname, '..'), 6 | displayName: 'node', 7 | testEnvironment: 'node', 8 | ...config, 9 | } 10 | -------------------------------------------------------------------------------- /tests/setup-env.js: -------------------------------------------------------------------------------- 1 | import {plugins} from 'pretty-format' 2 | import '../src/index' 3 | 4 | expect.addSnapshotSerializer(plugins.ConvertAnsi) 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "skipLibCheck": true 6 | }, 7 | "include": ["*.d.ts", "types"], 8 | "exclude": ["types/__tests__"] 9 | } 10 | -------------------------------------------------------------------------------- /types/__tests__/bun/bun-custom-expect-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from 'bun:test' 10 | import * as matchersStandalone from '../../matchers-standalone' 11 | import * as originalMatchers from '../../matchers' 12 | 13 | expect.extend(matchersStandalone) 14 | 15 | const element: HTMLElement = document.body 16 | 17 | function customExpect( 18 | _actual: HTMLElement, 19 | ): 20 | | originalMatchers.TestingLibraryMatchers 21 | | originalMatchers.TestingLibraryMatchers> { 22 | throw new Error('Method not implemented.') 23 | } 24 | 25 | customExpect(element).toBeInTheDOM() 26 | customExpect(element).toBeInTheDOM(document.body) 27 | customExpect(element).toBeInTheDocument() 28 | customExpect(element).toBeVisible() 29 | customExpect(element).toBeEmpty() 30 | customExpect(element).toBeDisabled() 31 | customExpect(element).toBeEnabled() 32 | customExpect(element).toBeInvalid() 33 | customExpect(element).toBeRequired() 34 | customExpect(element).toBeValid() 35 | customExpect(element).toContainElement(document.body) 36 | customExpect(element).toContainElement(null) 37 | customExpect(element).toContainHTML('body') 38 | customExpect(element).toHaveAttribute('attr') 39 | customExpect(element).toHaveAttribute('attr', true) 40 | customExpect(element).toHaveAttribute('attr', 'yes') 41 | customExpect(element).toHaveClass() 42 | customExpect(element).toHaveClass('cls1') 43 | customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 44 | customExpect(element).toHaveClass('cls1', {exact: true}) 45 | customExpect(element).toHaveDisplayValue('str') 46 | customExpect(element).toHaveDisplayValue(['str1', 'str2']) 47 | customExpect(element).toHaveDisplayValue(/str/) 48 | customExpect(element).toHaveDisplayValue([/str1/, 'str2']) 49 | customExpect(element).toHaveFocus() 50 | customExpect(element).toHaveFormValues({foo: 'bar', baz: 1}) 51 | customExpect(element).toHaveStyle('display: block') 52 | customExpect(element).toHaveStyle({display: 'block', width: 100}) 53 | customExpect(element).toHaveTextContent('Text') 54 | customExpect(element).toHaveTextContent(/Text/) 55 | customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 56 | customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 57 | customExpect(element).toHaveValue() 58 | customExpect(element).toHaveValue('str') 59 | customExpect(element).toHaveValue(['str1', 'str2']) 60 | customExpect(element).toHaveValue(1) 61 | customExpect(element).toHaveValue(null) 62 | customExpect(element).toBeChecked() 63 | customExpect(element).toHaveDescription('some description') 64 | customExpect(element).toHaveDescription(/some description/) 65 | customExpect(element).toHaveDescription(expect.stringContaining('partial')) 66 | customExpect(element).toHaveDescription() 67 | customExpect(element).toHaveAccessibleDescription('some description') 68 | customExpect(element).toHaveAccessibleDescription(/some description/) 69 | customExpect(element).toHaveAccessibleDescription( 70 | expect.stringContaining('partial'), 71 | ) 72 | customExpect(element).toHaveAccessibleDescription() 73 | 74 | customExpect(element).toHaveAccessibleErrorMessage() 75 | customExpect(element).toHaveAccessibleErrorMessage( 76 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 77 | ) 78 | customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i) 79 | customExpect(element).toHaveAccessibleErrorMessage( 80 | expect.stringContaining('Invalid time'), 81 | ) 82 | 83 | customExpect(element).toHaveAccessibleName('a label') 84 | customExpect(element).toHaveAccessibleName(/a label/) 85 | customExpect(element).toHaveAccessibleName( 86 | expect.stringContaining('partial label'), 87 | ) 88 | customExpect(element).toHaveAccessibleName() 89 | customExpect(element).toHaveErrorMessage( 90 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 91 | ) 92 | customExpect(element).toHaveErrorMessage(/invalid time/i) 93 | customExpect(element).toHaveErrorMessage( 94 | expect.stringContaining('Invalid time'), 95 | ) 96 | 97 | customExpect(element).toHaveRole('button') 98 | 99 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 100 | customExpect(element).nonExistentProperty() 101 | -------------------------------------------------------------------------------- /types/__tests__/bun/bun-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings for @types/jest work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from 'bun:test' 10 | import '../../bun' 11 | 12 | const element: HTMLElement = document.body 13 | 14 | expect(element).toBeInTheDOM() 15 | expect(element).toBeInTheDOM(document.body) 16 | expect(element).toBeInTheDocument() 17 | expect(element).toBeVisible() 18 | expect(element).toBeEmpty() 19 | expect(element).toBeDisabled() 20 | expect(element).toBeEnabled() 21 | expect(element).toBeInvalid() 22 | expect(element).toBeRequired() 23 | expect(element).toBeValid() 24 | expect(element).toContainElement(document.body) 25 | expect(element).toContainElement(null) 26 | expect(element).toContainHTML('body') 27 | expect(element).toHaveAttribute('attr') 28 | expect(element).toHaveAttribute('attr', true) 29 | expect(element).toHaveAttribute('attr', 'yes') 30 | expect(element).toHaveClass() 31 | expect(element).toHaveClass('cls1') 32 | expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 33 | expect(element).toHaveClass('cls1', {exact: true}) 34 | expect(element).toHaveDisplayValue('str') 35 | expect(element).toHaveDisplayValue(['str1', 'str2']) 36 | expect(element).toHaveDisplayValue(/str/) 37 | expect(element).toHaveDisplayValue([/str1/, 'str2']) 38 | expect(element).toHaveFocus() 39 | expect(element).toHaveFormValues({foo: 'bar', baz: 1}) 40 | expect(element).toHaveStyle('display: block') 41 | expect(element).toHaveStyle({display: 'block', width: 100}) 42 | expect(element).toHaveTextContent('Text') 43 | expect(element).toHaveTextContent(/Text/) 44 | expect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 45 | expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 46 | expect(element).toHaveValue() 47 | expect(element).toHaveValue('str') 48 | expect(element).toHaveValue(['str1', 'str2']) 49 | expect(element).toHaveValue(1) 50 | expect(element).toHaveValue(null) 51 | expect(element).toBeChecked() 52 | expect(element).toHaveDescription('some description') 53 | expect(element).toHaveDescription(/some description/) 54 | expect(element).toHaveDescription(expect.stringContaining('partial')) 55 | expect(element).toHaveDescription() 56 | expect(element).toHaveAccessibleDescription('some description') 57 | expect(element).toHaveAccessibleDescription(/some description/) 58 | expect(element).toHaveAccessibleDescription(expect.stringContaining('partial')) 59 | expect(element).toHaveAccessibleDescription() 60 | expect(element).toHaveAccessibleName('a label') 61 | expect(element).toHaveAccessibleName(/a label/) 62 | expect(element).toHaveAccessibleName(expect.stringContaining('partial label')) 63 | expect(element).toHaveAccessibleName() 64 | expect(element).toHaveErrorMessage( 65 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 66 | ) 67 | expect(element).toHaveErrorMessage(/invalid time/i) 68 | expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time')) 69 | expect(element).toHaveRole('button') 70 | 71 | expect(element).not.toBeInTheDOM() 72 | expect(element).not.toBeInTheDOM(document.body) 73 | expect(element).not.toBeInTheDocument() 74 | expect(element).not.toBeVisible() 75 | expect(element).not.toBeEmpty() 76 | expect(element).not.toBeEmptyDOMElement() 77 | expect(element).not.toBeDisabled() 78 | expect(element).not.toBeEnabled() 79 | expect(element).not.toBeInvalid() 80 | expect(element).not.toBeRequired() 81 | expect(element).not.toBeValid() 82 | expect(element).not.toContainElement(document.body) 83 | expect(element).not.toContainElement(null) 84 | expect(element).not.toContainHTML('body') 85 | expect(element).not.toHaveAttribute('attr') 86 | expect(element).not.toHaveAttribute('attr', true) 87 | expect(element).not.toHaveAttribute('attr', 'yes') 88 | expect(element).not.toHaveClass() 89 | expect(element).not.toHaveClass('cls1') 90 | expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 91 | expect(element).not.toHaveClass('cls1', {exact: true}) 92 | expect(element).not.toHaveDisplayValue('str') 93 | expect(element).not.toHaveDisplayValue(['str1', 'str2']) 94 | expect(element).not.toHaveDisplayValue(/str/) 95 | expect(element).not.toHaveDisplayValue([/str1/, 'str2']) 96 | expect(element).not.toHaveFocus() 97 | expect(element).not.toHaveFormValues({foo: 'bar', baz: 1}) 98 | expect(element).not.toHaveStyle('display: block') 99 | expect(element).not.toHaveTextContent('Text') 100 | expect(element).not.toHaveTextContent(/Text/) 101 | expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true}) 102 | expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true}) 103 | expect(element).not.toHaveValue() 104 | expect(element).not.toHaveValue('str') 105 | expect(element).not.toHaveValue(['str1', 'str2']) 106 | expect(element).not.toHaveValue(1) 107 | expect(element).not.toBeChecked() 108 | expect(element).not.toHaveDescription('some description') 109 | expect(element).not.toHaveDescription() 110 | expect(element).not.toHaveAccessibleDescription('some description') 111 | expect(element).not.toHaveAccessibleDescription() 112 | expect(element).not.toHaveAccessibleName('a label') 113 | expect(element).not.toHaveAccessibleName() 114 | expect(element).not.toBePartiallyChecked() 115 | expect(element).not.toHaveErrorMessage() 116 | expect(element).not.toHaveErrorMessage('Pikachu!') 117 | expect(element).not.toHaveRole('button') 118 | 119 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 120 | expect(element).nonExistentProperty() 121 | -------------------------------------------------------------------------------- /types/__tests__/bun/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "types": ["bun", "web"] 7 | }, 8 | "include": ["*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /types/__tests__/jest-globals/jest-globals-custom-expect-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from '@jest/globals' 10 | import * as matchers from '../../matchers' 11 | 12 | expect.extend(matchers) 13 | 14 | const element: HTMLElement = document.body 15 | 16 | function customExpect( 17 | _actual: HTMLElement, 18 | ): 19 | | matchers.TestingLibraryMatchers 20 | | matchers.TestingLibraryMatchers> { 21 | throw new Error('Method not implemented.') 22 | } 23 | 24 | customExpect(element).toBeInTheDOM() 25 | customExpect(element).toBeInTheDOM(document.body) 26 | customExpect(element).toBeInTheDocument() 27 | customExpect(element).toBeVisible() 28 | customExpect(element).toBeEmpty() 29 | customExpect(element).toBeDisabled() 30 | customExpect(element).toBeEnabled() 31 | customExpect(element).toBeInvalid() 32 | customExpect(element).toBeRequired() 33 | customExpect(element).toBeValid() 34 | customExpect(element).toContainElement(document.body) 35 | customExpect(element).toContainElement(null) 36 | customExpect(element).toContainHTML('body') 37 | customExpect(element).toHaveAttribute('attr') 38 | customExpect(element).toHaveAttribute('attr', true) 39 | customExpect(element).toHaveAttribute('attr', 'yes') 40 | customExpect(element).toHaveClass() 41 | customExpect(element).toHaveClass('cls1') 42 | customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 43 | customExpect(element).toHaveClass('cls1', {exact: true}) 44 | customExpect(element).toHaveDisplayValue('str') 45 | customExpect(element).toHaveDisplayValue(['str1', 'str2']) 46 | customExpect(element).toHaveDisplayValue(/str/) 47 | customExpect(element).toHaveDisplayValue([/str1/, 'str2']) 48 | customExpect(element).toHaveFocus() 49 | customExpect(element).toHaveFormValues({foo: 'bar', baz: 1}) 50 | customExpect(element).toHaveStyle('display: block') 51 | customExpect(element).toHaveStyle({display: 'block', width: 100}) 52 | customExpect(element).toHaveTextContent('Text') 53 | customExpect(element).toHaveTextContent(/Text/) 54 | customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 55 | customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 56 | customExpect(element).toHaveValue() 57 | customExpect(element).toHaveValue('str') 58 | customExpect(element).toHaveValue(['str1', 'str2']) 59 | customExpect(element).toHaveValue(1) 60 | customExpect(element).toHaveValue(null) 61 | customExpect(element).toBeChecked() 62 | customExpect(element).toHaveDescription('some description') 63 | customExpect(element).toHaveDescription(/some description/) 64 | customExpect(element).toHaveDescription(expect.stringContaining('partial')) 65 | customExpect(element).toHaveDescription() 66 | customExpect(element).toHaveAccessibleDescription('some description') 67 | customExpect(element).toHaveAccessibleDescription(/some description/) 68 | customExpect(element).toHaveAccessibleDescription( 69 | expect.stringContaining('partial'), 70 | ) 71 | customExpect(element).toHaveAccessibleDescription() 72 | 73 | customExpect(element).toHaveAccessibleErrorMessage() 74 | customExpect(element).toHaveAccessibleErrorMessage( 75 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 76 | ) 77 | customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i) 78 | customExpect(element).toHaveAccessibleErrorMessage( 79 | expect.stringContaining('Invalid time'), 80 | ) 81 | 82 | customExpect(element).toHaveAccessibleName('a label') 83 | customExpect(element).toHaveAccessibleName(/a label/) 84 | customExpect(element).toHaveAccessibleName( 85 | expect.stringContaining('partial label'), 86 | ) 87 | customExpect(element).toHaveAccessibleName() 88 | customExpect(element).toHaveErrorMessage( 89 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 90 | ) 91 | customExpect(element).toHaveErrorMessage(/invalid time/i) 92 | customExpect(element).toHaveErrorMessage( 93 | expect.stringContaining('Invalid time'), 94 | ) 95 | 96 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 97 | customExpect(element).nonExistentProperty() 98 | -------------------------------------------------------------------------------- /types/__tests__/jest-globals/jest-globals-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings for @types/jest work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from '@jest/globals' 10 | import '../../jest-globals' 11 | 12 | const element: HTMLElement = document.body 13 | 14 | expect(element).toBeInTheDOM() 15 | expect(element).toBeInTheDOM(document.body) 16 | expect(element).toBeInTheDocument() 17 | expect(element).toBeVisible() 18 | expect(element).toBeEmpty() 19 | expect(element).toBeDisabled() 20 | expect(element).toBeEnabled() 21 | expect(element).toBeInvalid() 22 | expect(element).toBeRequired() 23 | expect(element).toBeValid() 24 | expect(element).toContainElement(document.body) 25 | expect(element).toContainElement(null) 26 | expect(element).toContainHTML('body') 27 | expect(element).toHaveAttribute('attr') 28 | expect(element).toHaveAttribute('attr', true) 29 | expect(element).toHaveAttribute('attr', 'yes') 30 | expect(element).toHaveClass() 31 | expect(element).toHaveClass('cls1') 32 | expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 33 | expect(element).toHaveClass('cls1', {exact: true}) 34 | expect(element).toHaveDisplayValue('str') 35 | expect(element).toHaveDisplayValue(['str1', 'str2']) 36 | expect(element).toHaveDisplayValue(/str/) 37 | expect(element).toHaveDisplayValue([/str1/, 'str2']) 38 | expect(element).toHaveFocus() 39 | expect(element).toHaveFormValues({foo: 'bar', baz: 1}) 40 | expect(element).toHaveStyle('display: block') 41 | expect(element).toHaveStyle({display: 'block', width: 100}) 42 | expect(element).toHaveTextContent('Text') 43 | expect(element).toHaveTextContent(/Text/) 44 | expect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 45 | expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 46 | expect(element).toHaveValue() 47 | expect(element).toHaveValue('str') 48 | expect(element).toHaveValue(['str1', 'str2']) 49 | expect(element).toHaveValue(1) 50 | expect(element).toHaveValue(null) 51 | expect(element).toBeChecked() 52 | expect(element).toHaveDescription('some description') 53 | expect(element).toHaveDescription(/some description/) 54 | expect(element).toHaveDescription(expect.stringContaining('partial')) 55 | expect(element).toHaveDescription() 56 | expect(element).toHaveAccessibleDescription('some description') 57 | expect(element).toHaveAccessibleDescription(/some description/) 58 | expect(element).toHaveAccessibleDescription(expect.stringContaining('partial')) 59 | expect(element).toHaveAccessibleDescription() 60 | expect(element).toHaveAccessibleName('a label') 61 | expect(element).toHaveAccessibleName(/a label/) 62 | expect(element).toHaveAccessibleName(expect.stringContaining('partial label')) 63 | expect(element).toHaveAccessibleName() 64 | expect(element).toHaveErrorMessage( 65 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 66 | ) 67 | expect(element).toHaveErrorMessage(/invalid time/i) 68 | expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time')) 69 | expect(element).toHaveRole('button') 70 | 71 | expect(element).not.toBeInTheDOM() 72 | expect(element).not.toBeInTheDOM(document.body) 73 | expect(element).not.toBeInTheDocument() 74 | expect(element).not.toBeVisible() 75 | expect(element).not.toBeEmpty() 76 | expect(element).not.toBeEmptyDOMElement() 77 | expect(element).not.toBeDisabled() 78 | expect(element).not.toBeEnabled() 79 | expect(element).not.toBeInvalid() 80 | expect(element).not.toBeRequired() 81 | expect(element).not.toBeValid() 82 | expect(element).not.toContainElement(document.body) 83 | expect(element).not.toContainElement(null) 84 | expect(element).not.toContainHTML('body') 85 | expect(element).not.toHaveAttribute('attr') 86 | expect(element).not.toHaveAttribute('attr', true) 87 | expect(element).not.toHaveAttribute('attr', 'yes') 88 | expect(element).not.toHaveClass() 89 | expect(element).not.toHaveClass('cls1') 90 | expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 91 | expect(element).not.toHaveClass('cls1', {exact: true}) 92 | expect(element).not.toHaveDisplayValue('str') 93 | expect(element).not.toHaveDisplayValue(['str1', 'str2']) 94 | expect(element).not.toHaveDisplayValue(/str/) 95 | expect(element).not.toHaveDisplayValue([/str1/, 'str2']) 96 | expect(element).not.toHaveFocus() 97 | expect(element).not.toHaveFormValues({foo: 'bar', baz: 1}) 98 | expect(element).not.toHaveStyle('display: block') 99 | expect(element).not.toHaveTextContent('Text') 100 | expect(element).not.toHaveTextContent(/Text/) 101 | expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true}) 102 | expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true}) 103 | expect(element).not.toHaveValue() 104 | expect(element).not.toHaveValue('str') 105 | expect(element).not.toHaveValue(['str1', 'str2']) 106 | expect(element).not.toHaveValue(1) 107 | expect(element).not.toBeChecked() 108 | expect(element).not.toHaveDescription('some description') 109 | expect(element).not.toHaveDescription() 110 | expect(element).not.toHaveAccessibleDescription('some description') 111 | expect(element).not.toHaveAccessibleDescription() 112 | expect(element).not.toHaveAccessibleName('a label') 113 | expect(element).not.toHaveAccessibleName() 114 | expect(element).not.toBePartiallyChecked() 115 | expect(element).not.toHaveErrorMessage() 116 | expect(element).not.toHaveErrorMessage('Pikachu!') 117 | expect(element).not.toHaveRole('button') 118 | 119 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 120 | expect(element).nonExistentProperty() 121 | -------------------------------------------------------------------------------- /types/__tests__/jest-globals/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "types": [] 7 | }, 8 | "include": ["*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /types/__tests__/jest/jest-custom-expect-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import * as matchers from '../../matchers' 10 | 11 | expect.extend(matchers) 12 | 13 | const element: HTMLElement = document.body 14 | 15 | function customExpect( 16 | _actual: HTMLElement, 17 | ): 18 | | matchers.TestingLibraryMatchers 19 | | matchers.TestingLibraryMatchers> { 20 | throw new Error('Method not implemented.') 21 | } 22 | 23 | customExpect(element).toBeInTheDOM() 24 | customExpect(element).toBeInTheDOM(document.body) 25 | customExpect(element).toBeInTheDocument() 26 | customExpect(element).toBeVisible() 27 | customExpect(element).toBeEmpty() 28 | customExpect(element).toBeDisabled() 29 | customExpect(element).toBeEnabled() 30 | customExpect(element).toBeInvalid() 31 | customExpect(element).toBeRequired() 32 | customExpect(element).toBeValid() 33 | customExpect(element).toContainElement(document.body) 34 | customExpect(element).toContainElement(null) 35 | customExpect(element).toContainHTML('body') 36 | customExpect(element).toHaveAttribute('attr') 37 | customExpect(element).toHaveAttribute('attr', true) 38 | customExpect(element).toHaveAttribute('attr', 'yes') 39 | customExpect(element).toHaveClass() 40 | customExpect(element).toHaveClass('cls1') 41 | customExpect(element).toHaveClass(/cls/) 42 | customExpect(element).toHaveClass('cls1', 'cls2', /cls(3|4)/) 43 | customExpect(element).toHaveClass('cls1', {exact: true}) 44 | customExpect(element).toHaveDisplayValue('str') 45 | customExpect(element).toHaveDisplayValue(['str1', 'str2']) 46 | customExpect(element).toHaveDisplayValue(/str/) 47 | customExpect(element).toHaveDisplayValue([/str1/, 'str2']) 48 | customExpect(element).toHaveFocus() 49 | customExpect(element).toHaveFormValues({foo: 'bar', baz: 1}) 50 | customExpect(element).toHaveStyle('display: block') 51 | customExpect(element).toHaveStyle({display: 'block', width: 100}) 52 | customExpect(element).toHaveTextContent('Text') 53 | customExpect(element).toHaveTextContent(/Text/) 54 | customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 55 | customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 56 | customExpect(element).toHaveValue() 57 | customExpect(element).toHaveValue('str') 58 | customExpect(element).toHaveValue(['str1', 'str2']) 59 | customExpect(element).toHaveValue(1) 60 | customExpect(element).toHaveValue(null) 61 | customExpect(element).toBeChecked() 62 | customExpect(element).toHaveDescription('some description') 63 | customExpect(element).toHaveDescription(/some description/) 64 | customExpect(element).toHaveDescription(expect.stringContaining('partial')) 65 | customExpect(element).toHaveDescription() 66 | customExpect(element).toHaveAccessibleDescription('some description') 67 | customExpect(element).toHaveAccessibleDescription(/some description/) 68 | customExpect(element).toHaveAccessibleDescription( 69 | expect.stringContaining('partial'), 70 | ) 71 | customExpect(element).toHaveAccessibleDescription() 72 | 73 | customExpect(element).toHaveAccessibleErrorMessage() 74 | customExpect(element).toHaveAccessibleErrorMessage( 75 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 76 | ) 77 | customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i) 78 | customExpect(element).toHaveAccessibleErrorMessage( 79 | expect.stringContaining('Invalid time'), 80 | ) 81 | 82 | customExpect(element).toHaveAccessibleName('a label') 83 | customExpect(element).toHaveAccessibleName(/a label/) 84 | customExpect(element).toHaveAccessibleName( 85 | expect.stringContaining('partial label'), 86 | ) 87 | customExpect(element).toHaveAccessibleName() 88 | customExpect(element).toHaveErrorMessage( 89 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 90 | ) 91 | customExpect(element).toHaveErrorMessage(/invalid time/i) 92 | customExpect(element).toHaveErrorMessage( 93 | expect.stringContaining('Invalid time'), 94 | ) 95 | 96 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 97 | customExpect(element).nonExistentProperty() 98 | 99 | // @ts-expect-error 100 | customExpect(element).toHaveClass(/cls/, {exact: true}) 101 | -------------------------------------------------------------------------------- /types/__tests__/jest/jest-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings for @types/jest work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import '../../jest' 10 | 11 | const element: HTMLElement = document.body 12 | 13 | expect(element).toBeInTheDOM() 14 | expect(element).toBeInTheDOM(document.body) 15 | expect(element).toBeInTheDocument() 16 | expect(element).toBeVisible() 17 | expect(element).toBeEmpty() 18 | expect(element).toBeDisabled() 19 | expect(element).toBeEnabled() 20 | expect(element).toBeInvalid() 21 | expect(element).toBeRequired() 22 | expect(element).toBeValid() 23 | expect(element).toContainElement(document.body) 24 | expect(element).toContainElement(null) 25 | expect(element).toContainHTML('body') 26 | expect(element).toHaveAttribute('attr') 27 | expect(element).toHaveAttribute('attr', true) 28 | expect(element).toHaveAttribute('attr', 'yes') 29 | expect(element).toHaveClass() 30 | expect(element).toHaveClass('cls1') 31 | expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 32 | expect(element).toHaveClass('cls1', {exact: true}) 33 | expect(element).toHaveDisplayValue('str') 34 | expect(element).toHaveDisplayValue(['str1', 'str2']) 35 | expect(element).toHaveDisplayValue(/str/) 36 | expect(element).toHaveDisplayValue([/str1/, 'str2']) 37 | expect(element).toHaveFocus() 38 | expect(element).toHaveFormValues({foo: 'bar', baz: 1}) 39 | expect(element).toHaveStyle('display: block') 40 | expect(element).toHaveStyle({display: 'block', width: 100}) 41 | expect(element).toHaveTextContent('Text') 42 | expect(element).toHaveTextContent(/Text/) 43 | expect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 44 | expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 45 | expect(element).toHaveValue() 46 | expect(element).toHaveValue('str') 47 | expect(element).toHaveValue(['str1', 'str2']) 48 | expect(element).toHaveValue(1) 49 | expect(element).toHaveValue(null) 50 | expect(element).toBeChecked() 51 | expect(element).toHaveDescription('some description') 52 | expect(element).toHaveDescription(/some description/) 53 | expect(element).toHaveDescription(expect.stringContaining('partial')) 54 | expect(element).toHaveDescription() 55 | expect(element).toHaveAccessibleDescription('some description') 56 | expect(element).toHaveAccessibleDescription(/some description/) 57 | expect(element).toHaveAccessibleDescription(expect.stringContaining('partial')) 58 | expect(element).toHaveAccessibleDescription() 59 | expect(element).toHaveAccessibleName('a label') 60 | expect(element).toHaveAccessibleName(/a label/) 61 | expect(element).toHaveAccessibleName(expect.stringContaining('partial label')) 62 | expect(element).toHaveAccessibleName() 63 | expect(element).toHaveErrorMessage( 64 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 65 | ) 66 | expect(element).toHaveErrorMessage(/invalid time/i) 67 | expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time')) 68 | expect(element).toHaveRole('button') 69 | 70 | expect(element).not.toBeInTheDOM() 71 | expect(element).not.toBeInTheDOM(document.body) 72 | expect(element).not.toBeInTheDocument() 73 | expect(element).not.toBeVisible() 74 | expect(element).not.toBeEmpty() 75 | expect(element).not.toBeEmptyDOMElement() 76 | expect(element).not.toBeDisabled() 77 | expect(element).not.toBeEnabled() 78 | expect(element).not.toBeInvalid() 79 | expect(element).not.toBeRequired() 80 | expect(element).not.toBeValid() 81 | expect(element).not.toContainElement(document.body) 82 | expect(element).not.toContainElement(null) 83 | expect(element).not.toContainHTML('body') 84 | expect(element).not.toHaveAttribute('attr') 85 | expect(element).not.toHaveAttribute('attr', true) 86 | expect(element).not.toHaveAttribute('attr', 'yes') 87 | expect(element).not.toHaveClass() 88 | expect(element).not.toHaveClass('cls1') 89 | expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 90 | expect(element).not.toHaveClass('cls1', {exact: true}) 91 | expect(element).not.toHaveDisplayValue('str') 92 | expect(element).not.toHaveDisplayValue(['str1', 'str2']) 93 | expect(element).not.toHaveDisplayValue(/str/) 94 | expect(element).not.toHaveDisplayValue([/str1/, 'str2']) 95 | expect(element).not.toHaveFocus() 96 | expect(element).not.toHaveFormValues({foo: 'bar', baz: 1}) 97 | expect(element).not.toHaveStyle('display: block') 98 | expect(element).not.toHaveTextContent('Text') 99 | expect(element).not.toHaveTextContent(/Text/) 100 | expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true}) 101 | expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true}) 102 | expect(element).not.toHaveValue() 103 | expect(element).not.toHaveValue('str') 104 | expect(element).not.toHaveValue(['str1', 'str2']) 105 | expect(element).not.toHaveValue(1) 106 | expect(element).not.toBeChecked() 107 | expect(element).not.toHaveDescription('some description') 108 | expect(element).not.toHaveDescription() 109 | expect(element).not.toHaveAccessibleDescription('some description') 110 | expect(element).not.toHaveAccessibleDescription() 111 | expect(element).not.toHaveAccessibleName('a label') 112 | expect(element).not.toHaveAccessibleName() 113 | expect(element).not.toBePartiallyChecked() 114 | expect(element).not.toHaveErrorMessage() 115 | expect(element).not.toHaveErrorMessage('Pikachu!') 116 | expect(element).not.toHaveRole('button') 117 | 118 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 119 | expect(element).nonExistentProperty() 120 | -------------------------------------------------------------------------------- /types/__tests__/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "types": ["jest"] 7 | }, 8 | "include": ["*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /types/__tests__/vitest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "types": [] 7 | }, 8 | "include": ["*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /types/__tests__/vitest/vitest-custom-expect-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from 'vitest' 10 | import * as matchers from '../../matchers' 11 | 12 | expect.extend(matchers) 13 | 14 | const element: HTMLElement = document.body 15 | 16 | function customExpect( 17 | _actual: HTMLElement, 18 | ): 19 | | matchers.TestingLibraryMatchers 20 | | matchers.TestingLibraryMatchers> { 21 | throw new Error('Method not implemented.') 22 | } 23 | 24 | customExpect(element).toBeInTheDOM() 25 | customExpect(element).toBeInTheDOM(document.body) 26 | customExpect(element).toBeInTheDocument() 27 | customExpect(element).toBeVisible() 28 | customExpect(element).toBeEmpty() 29 | customExpect(element).toBeDisabled() 30 | customExpect(element).toBeEnabled() 31 | customExpect(element).toBeInvalid() 32 | customExpect(element).toBeRequired() 33 | customExpect(element).toBeValid() 34 | customExpect(element).toContainElement(document.body) 35 | customExpect(element).toContainElement(null) 36 | customExpect(element).toContainHTML('body') 37 | customExpect(element).toHaveAttribute('attr') 38 | customExpect(element).toHaveAttribute('attr', true) 39 | customExpect(element).toHaveAttribute('attr', 'yes') 40 | customExpect(element).toHaveClass() 41 | customExpect(element).toHaveClass('cls1') 42 | customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 43 | customExpect(element).toHaveClass('cls1', {exact: true}) 44 | customExpect(element).toHaveDisplayValue('str') 45 | customExpect(element).toHaveDisplayValue(['str1', 'str2']) 46 | customExpect(element).toHaveDisplayValue(/str/) 47 | customExpect(element).toHaveDisplayValue([/str1/, 'str2']) 48 | customExpect(element).toHaveFocus() 49 | customExpect(element).toHaveFormValues({foo: 'bar', baz: 1}) 50 | customExpect(element).toHaveStyle('display: block') 51 | customExpect(element).toHaveStyle({display: 'block', width: 100}) 52 | customExpect(element).toHaveTextContent('Text') 53 | customExpect(element).toHaveTextContent(/Text/) 54 | customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 55 | customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 56 | customExpect(element).toHaveValue() 57 | customExpect(element).toHaveValue('str') 58 | customExpect(element).toHaveValue(['str1', 'str2']) 59 | customExpect(element).toHaveValue(1) 60 | customExpect(element).toHaveValue(null) 61 | customExpect(element).toBeChecked() 62 | customExpect(element).toHaveDescription('some description') 63 | customExpect(element).toHaveDescription(/some description/) 64 | customExpect(element).toHaveDescription(expect.stringContaining('partial')) 65 | customExpect(element).toHaveDescription() 66 | customExpect(element).toHaveAccessibleDescription('some description') 67 | customExpect(element).toHaveAccessibleDescription(/some description/) 68 | customExpect(element).toHaveAccessibleDescription( 69 | expect.stringContaining('partial'), 70 | ) 71 | customExpect(element).toHaveAccessibleDescription() 72 | 73 | customExpect(element).toHaveAccessibleErrorMessage() 74 | customExpect(element).toHaveAccessibleErrorMessage( 75 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 76 | ) 77 | customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i) 78 | customExpect(element).toHaveAccessibleErrorMessage( 79 | expect.stringContaining('Invalid time'), 80 | ) 81 | 82 | customExpect(element).toHaveAccessibleName('a label') 83 | customExpect(element).toHaveAccessibleName(/a label/) 84 | customExpect(element).toHaveAccessibleName( 85 | expect.stringContaining('partial label'), 86 | ) 87 | customExpect(element).toHaveAccessibleName() 88 | customExpect(element).toHaveErrorMessage( 89 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 90 | ) 91 | customExpect(element).toHaveErrorMessage(/invalid time/i) 92 | customExpect(element).toHaveErrorMessage( 93 | expect.stringContaining('Invalid time'), 94 | ) 95 | 96 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 97 | customExpect(element).nonExistentProperty() 98 | -------------------------------------------------------------------------------- /types/__tests__/vitest/vitest-types.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File that tests whether the TypeScript typings for @types/jest work as expected. 3 | */ 4 | 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | /* eslint-disable @typescript-eslint/no-floating-promises */ 7 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 8 | 9 | import {expect} from 'vitest' 10 | import '../../vitest' 11 | 12 | const element: HTMLElement = document.body 13 | 14 | expect(element).toBeInTheDOM() 15 | expect(element).toBeInTheDOM(document.body) 16 | expect(element).toBeInTheDocument() 17 | expect(element).toBeVisible() 18 | expect(element).toBeEmpty() 19 | expect(element).toBeDisabled() 20 | expect(element).toBeEnabled() 21 | expect(element).toBeInvalid() 22 | expect(element).toBeRequired() 23 | expect(element).toBeValid() 24 | expect(element).toContainElement(document.body) 25 | expect(element).toContainElement(null) 26 | expect(element).toContainHTML('body') 27 | expect(element).toHaveAttribute('attr') 28 | expect(element).toHaveAttribute('attr', true) 29 | expect(element).toHaveAttribute('attr', 'yes') 30 | expect(element).toHaveClass() 31 | expect(element).toHaveClass('cls1') 32 | expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 33 | expect(element).toHaveClass('cls1', {exact: true}) 34 | expect(element).toHaveDisplayValue('str') 35 | expect(element).toHaveDisplayValue(['str1', 'str2']) 36 | expect(element).toHaveDisplayValue(/str/) 37 | expect(element).toHaveDisplayValue([/str1/, 'str2']) 38 | expect(element).toHaveFocus() 39 | expect(element).toHaveFormValues({foo: 'bar', baz: 1}) 40 | expect(element).toHaveStyle('display: block') 41 | expect(element).toHaveStyle({display: 'block', width: 100}) 42 | expect(element).toHaveTextContent('Text') 43 | expect(element).toHaveTextContent(/Text/) 44 | expect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) 45 | expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) 46 | expect(element).toHaveValue() 47 | expect(element).toHaveValue('str') 48 | expect(element).toHaveValue(['str1', 'str2']) 49 | expect(element).toHaveValue(1) 50 | expect(element).toHaveValue(null) 51 | expect(element).toBeChecked() 52 | expect(element).toHaveDescription('some description') 53 | expect(element).toHaveDescription(/some description/) 54 | expect(element).toHaveDescription(expect.stringContaining('partial')) 55 | expect(element).toHaveDescription() 56 | expect(element).toHaveAccessibleDescription('some description') 57 | expect(element).toHaveAccessibleDescription(/some description/) 58 | expect(element).toHaveAccessibleDescription(expect.stringContaining('partial')) 59 | expect(element).toHaveAccessibleDescription() 60 | expect(element).toHaveAccessibleName('a label') 61 | expect(element).toHaveAccessibleName(/a label/) 62 | expect(element).toHaveAccessibleName(expect.stringContaining('partial label')) 63 | expect(element).toHaveAccessibleName() 64 | expect(element).toHaveErrorMessage( 65 | 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 66 | ) 67 | expect(element).toHaveErrorMessage(/invalid time/i) 68 | expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time')) 69 | expect(element).toHaveRole('button') 70 | 71 | expect(element).not.toBeInTheDOM() 72 | expect(element).not.toBeInTheDOM(document.body) 73 | expect(element).not.toBeInTheDocument() 74 | expect(element).not.toBeVisible() 75 | expect(element).not.toBeEmpty() 76 | expect(element).not.toBeEmptyDOMElement() 77 | expect(element).not.toBeDisabled() 78 | expect(element).not.toBeEnabled() 79 | expect(element).not.toBeInvalid() 80 | expect(element).not.toBeRequired() 81 | expect(element).not.toBeValid() 82 | expect(element).not.toContainElement(document.body) 83 | expect(element).not.toContainElement(null) 84 | expect(element).not.toContainHTML('body') 85 | expect(element).not.toHaveAttribute('attr') 86 | expect(element).not.toHaveAttribute('attr', true) 87 | expect(element).not.toHaveAttribute('attr', 'yes') 88 | expect(element).not.toHaveClass() 89 | expect(element).not.toHaveClass('cls1') 90 | expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4') 91 | expect(element).not.toHaveClass('cls1', {exact: true}) 92 | expect(element).not.toHaveDisplayValue('str') 93 | expect(element).not.toHaveDisplayValue(['str1', 'str2']) 94 | expect(element).not.toHaveDisplayValue(/str/) 95 | expect(element).not.toHaveDisplayValue([/str1/, 'str2']) 96 | expect(element).not.toHaveFocus() 97 | expect(element).not.toHaveFormValues({foo: 'bar', baz: 1}) 98 | expect(element).not.toHaveStyle('display: block') 99 | expect(element).not.toHaveTextContent('Text') 100 | expect(element).not.toHaveTextContent(/Text/) 101 | expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true}) 102 | expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true}) 103 | expect(element).not.toHaveValue() 104 | expect(element).not.toHaveValue('str') 105 | expect(element).not.toHaveValue(['str1', 'str2']) 106 | expect(element).not.toHaveValue(1) 107 | expect(element).not.toBeChecked() 108 | expect(element).not.toHaveDescription('some description') 109 | expect(element).not.toHaveDescription() 110 | expect(element).not.toHaveAccessibleDescription('some description') 111 | expect(element).not.toHaveAccessibleDescription() 112 | expect(element).not.toHaveAccessibleName('a label') 113 | expect(element).not.toHaveAccessibleName() 114 | expect(element).not.toBePartiallyChecked() 115 | expect(element).not.toHaveErrorMessage() 116 | expect(element).not.toHaveErrorMessage('Pikachu!') 117 | expect(element).not.toHaveRole('button') 118 | 119 | // @ts-expect-error The types accidentally allowed any property by falling back to "any" 120 | expect(element).nonExistentProperty() 121 | -------------------------------------------------------------------------------- /types/bun.d.ts: -------------------------------------------------------------------------------- 1 | import {type expect} from 'bun:test' 2 | import {type TestingLibraryMatchers} from './matchers' 3 | 4 | export {} 5 | declare module 'bun:test' { 6 | interface Matchers 7 | extends TestingLibraryMatchers< 8 | ReturnType, 9 | T 10 | > {} 11 | } 12 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /types/jest-globals.d.ts: -------------------------------------------------------------------------------- 1 | import {type expect} from '@jest/globals' 2 | import {type TestingLibraryMatchers} from './matchers' 3 | 4 | export {} 5 | declare module '@jest/expect' { 6 | export interface Matchers> 7 | extends TestingLibraryMatchers< 8 | ReturnType, 9 | R 10 | > {} 11 | } 12 | -------------------------------------------------------------------------------- /types/jest.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {type TestingLibraryMatchers} from './matchers' 4 | 5 | declare global { 6 | namespace jest { 7 | interface Matchers 8 | extends TestingLibraryMatchers< 9 | ReturnType, 10 | R 11 | > {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/matchers-standalone.d.ts: -------------------------------------------------------------------------------- 1 | import {type TestingLibraryMatchers as _TLM} from './matchers' 2 | 3 | interface MatcherReturnType { 4 | pass: boolean 5 | message: () => string 6 | } 7 | 8 | interface OverloadedMatchers { 9 | toHaveClass(expected: any, ...rest: string[]) : MatcherReturnType 10 | toHaveClass( 11 | expected: any, 12 | className: string, 13 | options?: {exact: boolean}, 14 | ) : MatcherReturnType 15 | } 16 | 17 | declare namespace matchersStandalone { 18 | type MatchersStandalone = { 19 | [T in keyof _TLM]: ( 20 | expected: any, 21 | ...rest: Parameters<_TLM[T]> 22 | ) => MatcherReturnType 23 | } & OverloadedMatchers 24 | 25 | type TestingLibraryMatchers = _TLM 26 | } 27 | 28 | declare const matchersStandalone: matchersStandalone.MatchersStandalone & 29 | Record 30 | export = matchersStandalone 31 | -------------------------------------------------------------------------------- /types/vitest.d.ts: -------------------------------------------------------------------------------- 1 | import 'vitest' 2 | import {type TestingLibraryMatchers} from './matchers' 3 | 4 | declare module 'vitest' { 5 | interface Assertion 6 | extends TestingLibraryMatchers< 7 | any, 8 | T 9 | > {} 10 | interface AsymmetricMatchersContaining 11 | extends TestingLibraryMatchers< 12 | any, 13 | any 14 | > {} 15 | } 16 | -------------------------------------------------------------------------------- /vitest.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vitest.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'vitest' 2 | import * as extensions from './dist/matchers' 3 | 4 | expect.extend(extensions) 5 | --------------------------------------------------------------------------------