├── .gitattributes ├── .eslintignore ├── .npmrc ├── index.js ├── other ├── sloth.png ├── CODE_OF_CONDUCT.md └── MAINTAINING.md ├── .nycrc ├── src ├── __tests__ │ ├── helpers │ │ ├── babel.js │ │ ├── jsdom.js │ │ ├── matchers.js │ │ ├── renderer.js │ │ └── reporter.js │ ├── toHaveFocus.test.js │ ├── toBeEmptyDOMElement.test.js │ ├── toHaveAttribute.test.js │ ├── toBeInTheDocument.test.js │ ├── utils.test.js │ ├── toBeInvalid.test.js │ ├── toContainElement.test.js │ ├── toHaveTextContent.test.js │ ├── toHaveAccessibleDescription.test.js │ ├── toBeDisabled.test.js │ ├── toBeRequired.test.js │ ├── toBeValid.test.js │ ├── toHaveDescription.test.js │ ├── toHaveStyle.test.js │ ├── toHaveDisplayValue.test.js │ ├── toBePartiallyChecked.test.js │ ├── toHaveValue.test.js │ ├── toContainHTML.test.js │ ├── toBeEnabled.test.js │ ├── toHaveErrorMessage.test.js │ ├── toBeVisible.test.js │ ├── toHaveClassName.test.js │ └── toBeChecked.test.js ├── printers.js ├── toHaveFocus.js ├── toHaveAccessibleName.js ├── toBeEmptyDOMElement.js ├── toContainElement.js ├── toBeInTheDocument.js ├── index.js ├── toHaveAccessibleDescription.js ├── toHaveAttribute.js ├── toContainHTML.js ├── toHaveTextContent.js ├── toHaveErrorMessage.js ├── toBePartiallyChecked.js ├── toBeVisible.js ├── toBeRequired.js ├── toHaveDescription.js ├── utils.js ├── toBeInvalid.js ├── toHaveValue.js ├── toBeChecked.js ├── toHaveDisplayValue.js ├── toBeDisabled.js ├── toHaveStyle.js ├── toHaveFormValues.js └── toHaveClassName.js ├── .gitignore ├── .babelrc ├── .prettierrc ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── Question.md │ ├── Bug_Report.md │ └── Feature_Request.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── jasmine.json ├── .eslintrc ├── LICENSE ├── package.json └── .all-contributorsrc /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | results -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=false -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import JasmineDOM from './src'; 2 | 3 | export default JasmineDOM; 4 | -------------------------------------------------------------------------------- /other/sloth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/jasmine-dom/HEAD/other/sloth.png -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "src/__tests__", 4 | "index.js" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/__tests__/helpers/babel.js: -------------------------------------------------------------------------------- 1 | require('regenerator-runtime/runtime.js'); 2 | require('@babel/register'); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | results 7 | *.tgz 8 | yarn.lock 9 | package-lock.json -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "env": { 4 | "test": { 5 | "plugins": ["istanbul"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/helpers/jsdom.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | 3 | const { window } = new JSDOM(''); 4 | 5 | export default window.document; 6 | -------------------------------------------------------------------------------- /src/__tests__/helpers/matchers.js: -------------------------------------------------------------------------------- 1 | import JasmineDOM from '../../../index'; 2 | 3 | beforeAll(() => { 4 | jasmine.getEnv().addMatchers(JasmineDOM); 5 | }); 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "arrowParens": "avoid", 8 | "useTabs": true 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "src/__tests__", 3 | "spec_files": [ 4 | "**/*.test.js" 5 | ], 6 | "helpers": [ 7 | "helpers/babel.js", 8 | "helpers/**/*.js" 9 | ], 10 | "stopSpecOnExpectationFailure": false, 11 | "random": false 12 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:prettier/recommended" 4 | ], 5 | "plugins": [ 6 | "prettier" 7 | ], 8 | "rules": { 9 | "prettier/prettier": "error" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2020, 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/__tests__/helpers/renderer.js: -------------------------------------------------------------------------------- 1 | import document from './jsdom'; 2 | 3 | export function render(html) { 4 | const container = document.createElement('div'); 5 | container.innerHTML = html; 6 | const queryByTestId = testId => { 7 | return container.querySelector(`[data-testid="${testId}"]`); 8 | }; 9 | document.body.innerHTML = ''; 10 | document.body.appendChild(container); 11 | return { 12 | container, 13 | queryByTestId, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/__tests__/helpers/reporter.js: -------------------------------------------------------------------------------- 1 | import { SpecReporter } from 'jasmine-spec-reporter'; 2 | import { JUnitXmlReporter } from 'jasmine-reporters'; 3 | 4 | jasmine.getEnv().clearReporters(); 5 | 6 | jasmine.getEnv().addReporter( 7 | new SpecReporter({ 8 | spec: { 9 | displayPending: true, 10 | }, 11 | summary: { 12 | displayFailed: true, 13 | }, 14 | }) 15 | ); 16 | 17 | jasmine.getEnv().addReporter( 18 | new JUnitXmlReporter({ 19 | consolidateAll: true, 20 | savePath: 'results', 21 | filePrefix: 'results', 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/printers.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | function printError(message) { 4 | return chalk.bgRedBright.black(message); 5 | } 6 | 7 | function printSecError(message) { 8 | return chalk.redBright(message); 9 | } 10 | 11 | function printSuccess(message) { 12 | return chalk.bgGreenBright.black(message); 13 | } 14 | 15 | function printSecSuccess(message) { 16 | return chalk.greenBright(message); 17 | } 18 | 19 | function printWarning(message) { 20 | return chalk.bgYellow.black(message); 21 | } 22 | 23 | function printSecWarning(message) { 24 | return chalk.yellow(message); 25 | } 26 | 27 | export { printError, printSuccess, printWarning, printSecError, printSecSuccess, printSecWarning }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `jasmine-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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brian Alexis Michel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/__tests__/toHaveFocus.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import document from './helpers/jsdom'; 3 | import { toHaveFocus } from '../toHaveFocus'; 4 | 5 | describe('.toHaveFocus', () => { 6 | const { compare, negativeCompare } = toHaveFocus(); 7 | const { container } = render(` 8 |
9 | 10 | 11 | 12 |
13 | `); 14 | 15 | const focused = container.querySelector('#focused'); 16 | const unfocused = container.querySelector('#unfocused'); 17 | 18 | it('positive test cases', () => { 19 | document.body.appendChild(container); 20 | focused.focus(); 21 | 22 | expect(focused).toHaveFocus(); 23 | expect(unfocused).not.toHaveFocus(); 24 | }); 25 | 26 | it('negative test cases', () => { 27 | const { message: negativeMessage, pass: negativePass } = negativeCompare(focused); 28 | const { message: positiveMessage, pass: positivePass } = compare(unfocused); 29 | 30 | expect(negativePass).toBeFalse(); 31 | expect(negativeMessage).toMatch(/Expected.*not to have focus\./); 32 | 33 | expect(positivePass).toBeFalse(); 34 | expect(positiveMessage).toMatch(/Expected the provided.*button.* element to have focus\./); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/toHaveFocus.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSecSuccess, printSuccess, printSecError, printError } from './printers'; 3 | 4 | export function toHaveFocus() { 5 | return { 6 | compare: function (htmlElement) { 7 | checkHtmlElement(htmlElement); 8 | let result = {}; 9 | result.pass = htmlElement.ownerDocument.activeElement === htmlElement; 10 | result.message = result.pass 11 | ? `${printSuccess('PASSED')} ${printSecSuccess( 12 | `Expected the provided ${printSuccess(getTag(htmlElement))} element to have focus.` 13 | )}` 14 | : `${printError('FAILED')} ${printSecError( 15 | `Expected the provided ${printError(getTag(htmlElement))} element to have focus.` 16 | )}`; 17 | return result; 18 | }, 19 | negativeCompare: function (htmlElement) { 20 | checkHtmlElement(htmlElement); 21 | let result = {}; 22 | result.pass = htmlElement.ownerDocument.activeElement !== htmlElement; 23 | result.message = result.pass 24 | ? `${printSuccess('PASSED')} ${printSecSuccess( 25 | `Expected the provided ${printSuccess(getTag(htmlElement))} element not to have focus.` 26 | )}` 27 | : `${printError('FAILED')} ${printSecError( 28 | `Expected the provided ${printError(getTag(htmlElement))} element not to have focus.` 29 | )}`; 30 | return result; 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.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/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Bugs, missing documentation, or unexpected behavior 🤔. 4 | --- 5 | 6 | 20 | 21 | - `jasmine-dom` version: 22 | - `node` version: 23 | - `npm` (or `yarn`) version: 24 | 25 | ### Relevant code or config: 26 | 27 | ```js 28 | var your => (code) => here; 29 | ``` 30 | 31 | ### What you did: 32 | 33 | 34 | 35 | ### What happened: 36 | 37 | 38 | 39 | ### Reproduction: 40 | 41 | 47 | 48 | ### Problem description: 49 | 50 | 51 | 52 | ### Suggested solution: 53 | 54 | 58 | -------------------------------------------------------------------------------- /src/toHaveAccessibleName.js: -------------------------------------------------------------------------------- 1 | import { computeAccessibleName } from 'dom-accessibility-api'; 2 | import { checkHtmlElement } from './utils'; 3 | import { printSuccess, printSecSuccess, printSecError, printError } from './printers'; 4 | 5 | /** 6 | * @param {jasmine.MatchersUtil} matchersUtil 7 | */ 8 | export function toHaveAccessibleName(matchersUtil) { 9 | return { 10 | compare: function (htmlElement, expectedAccessibleName) { 11 | checkHtmlElement(htmlElement); 12 | const actualAccessibleName = computeAccessibleName(htmlElement); 13 | const missingExpectedValue = arguments.length === 1; 14 | 15 | let pass = false; 16 | if (missingExpectedValue) { 17 | // When called without an expected value we only want to validate that the element has an 18 | // accessible name, whatever it may be. 19 | pass = actualAccessibleName !== ''; 20 | } else { 21 | pass = 22 | expectedAccessibleName instanceof RegExp 23 | ? expectedAccessibleName.test(actualAccessibleName) 24 | : matchersUtil.equals(actualAccessibleName, expectedAccessibleName); 25 | } 26 | 27 | return { 28 | pass, 29 | message: pass 30 | ? `${printSuccess('PASSED')} ${printSecSuccess( 31 | `Expected element to have accessible name:\n${printSuccess( 32 | String(expectedAccessibleName) 33 | )}\nReceived:\n${printSuccess(actualAccessibleName)}` 34 | )}` 35 | : `${printError('FAILED')} ${printSecError( 36 | `Expected element to have accessible name:\n${printError( 37 | String(expectedAccessibleName) 38 | )}\nReceived:\n${printSuccess(actualAccessibleName)}` 39 | )}`, 40 | }; 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/toBeEmptyDOMElement.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printError, printSecError, printSuccess, printSecSuccess } from './printers'; 3 | 4 | export function toBeEmptyDOMElement() { 5 | return { 6 | compare: function (htmlElement) { 7 | checkHtmlElement(htmlElement); 8 | let result = {}; 9 | result.pass = htmlElement.innerHTML === ''; 10 | result.message = `${ 11 | result.pass 12 | ? `${printSuccess('PASSED')} ${printSecSuccess( 13 | `Expected ${printSuccess(getTag(htmlElement))} to be an empty DOM element. Received: ${printSuccess( 14 | `'${htmlElement.innerHTML}'` 15 | )}.` 16 | )}` 17 | : `${printError('FAILED')} ${printSecError( 18 | `Expected ${printError(getTag(htmlElement))} to be an empty DOM element. Received: ${printError( 19 | `'${htmlElement.innerHTML}'` 20 | )}.` 21 | )}` 22 | }`; 23 | return result; 24 | }, 25 | negativeCompare: function (htmlElement) { 26 | checkHtmlElement(htmlElement); 27 | let result = {}; 28 | result.pass = htmlElement.innerHTML !== ''; 29 | result.message = `${ 30 | result.pass 31 | ? `${printSuccess('PASSED')} ${printSecSuccess( 32 | `Expected ${printSuccess(getTag(htmlElement))} not to be an empty DOM element. Received: ${printSuccess( 33 | `'${htmlElement.innerHTML}'` 34 | )}` 35 | )}` 36 | : `${printError('FAILED')} ${printSecError( 37 | `Expected ${printError(getTag(htmlElement))} not to be an empty DOM element. Received: ${printError( 38 | `'${htmlElement.innerHTML}'` 39 | )}.` 40 | )}` 41 | }`; 42 | return result; 43 | }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/toContainElement.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSecSuccess, printSuccess, printError, printSecError } from './printers'; 3 | 4 | export function toContainElement() { 5 | return { 6 | compare: function (container, htmlElement) { 7 | checkHtmlElement(container); 8 | let result = {}; 9 | if (htmlElement !== null) { 10 | checkHtmlElement(htmlElement); 11 | } 12 | result.pass = container.contains(htmlElement); 13 | result.message = `${ 14 | result.pass 15 | ? `${printSuccess('PASSED')} ${printSecSuccess( 16 | `Expected the element ${printSuccess(getTag(container))} to contain ${printSuccess(getTag(htmlElement))}` 17 | )}` 18 | : `${printError('FAILED')} ${printSecError( 19 | `Expected the element ${printError(getTag(container))} to contain ${printError(getTag(htmlElement))}` 20 | )}` 21 | }`; 22 | return result; 23 | }, 24 | negativeCompare: function (container, htmlElement) { 25 | checkHtmlElement(container); 26 | let result = {}; 27 | if (htmlElement !== null) { 28 | checkHtmlElement(htmlElement); 29 | } 30 | result.pass = !container.contains(htmlElement); 31 | result.message = `${ 32 | result.pass 33 | ? `${printSuccess('PASSED')} ${printSecSuccess( 34 | `Expected the element ${printSuccess(getTag(container))} not to contain ${printSuccess( 35 | getTag(htmlElement) 36 | )}` 37 | )}` 38 | : `${printError('FAILED')} ${printSecError( 39 | `Expected the element ${printError(getTag(container))} not to contain ${printError(getTag(htmlElement))}` 40 | )}` 41 | }`; 42 | return result; 43 | }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/__tests__/toBeEmptyDOMElement.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeEmptyDOMElement } from '../toBeEmptyDOMElement'; 3 | 4 | describe('.toBeEmptyDOMElement', () => { 5 | const { compare, negativeCompare } = toBeEmptyDOMElement(); 6 | const { queryByTestId } = render(` 7 | 8 | 9 | 10 | 11 | `); 12 | const notAnElement = { whatever: 'clearly not an element' }; 13 | 14 | it('positive compare', () => { 15 | expect(queryByTestId('empty-span')).toBeEmptyDOMElement(); 16 | expect(queryByTestId('empty-svg')).toBeEmptyDOMElement(); 17 | }); 18 | 19 | it('negative compare', () => { 20 | expect(queryByTestId('not-empty-span')).not.toBeEmptyDOMElement(); 21 | }); 22 | 23 | it('negative test cases', () => { 24 | const { message: negativeMessage, pass: negativePass } = negativeCompare(queryByTestId('empty-span')); 25 | const { message: emptySVGMessage, pass: emptySVGPass } = negativeCompare(queryByTestId('empty-svg')); 26 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('not-empty-span')); 27 | 28 | expect(negativePass).toBeFalse(); 29 | expect(negativeMessage).toMatch(/Expected.*not to be an empty DOM element\./); 30 | expect(emptySVGPass).toBeFalse(); 31 | expect(emptySVGMessage).toMatch(/Expected.*not to be an empty DOM element\./); 32 | 33 | expect(positivePass).toBeFalse(); 34 | expect(positiveMessage).toMatch(/Expected.*to be an empty DOM element\./); 35 | 36 | expect(() => expect(notAnElement).toBeEmptyDOMElement()).toThrowError(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/toBeInTheDocument.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSuccess, printSecSuccess, printError, printSecError } from './printers'; 3 | 4 | export function toBeInTheDocument() { 5 | return { 6 | compare: function (htmlElement) { 7 | if (htmlElement !== null) { 8 | checkHtmlElement(htmlElement); 9 | } 10 | let result = {}; 11 | result.pass = htmlElement === null ? false : htmlElement.ownerDocument.contains(htmlElement); 12 | result.message = `${ 13 | result.pass 14 | ? `${printSuccess('PASSED')} ${printSecSuccess( 15 | `Expected the ${printSuccess(getTag(htmlElement))} element to be in the document and it ${printSuccess( 16 | 'is in the document' 17 | )}.` 18 | )}` 19 | : `${printError('FAILED')} ${printSecError( 20 | `The ${printError(getTag(htmlElement))} element provided ${printError( 21 | 'could not be found in the document' 22 | )}.` 23 | )}` 24 | }`; 25 | return result; 26 | }, 27 | negativeCompare: function (htmlElement) { 28 | if (htmlElement !== null) { 29 | checkHtmlElement(htmlElement); 30 | } 31 | let result = {}; 32 | result.pass = htmlElement === null ? true : !htmlElement.ownerDocument.contains(htmlElement); 33 | result.message = `${ 34 | result.pass 35 | ? `${printSuccess('PASSED')} ${printSecSuccess( 36 | `Expected the document not to contain the provided ${printSuccess( 37 | htmlElement !== null ? getTag(htmlElement) : null 38 | )} element.` 39 | )}` 40 | : `${printError('FAILED')} ${printSecError( 41 | `Expected the document not to contain the provided ${printError(getTag(htmlElement))} element.` 42 | )}` 43 | }`; 44 | return result; 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/__tests__/toHaveAttribute.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveAttribute } from '../toHaveAttribute'; 3 | 4 | describe('.toHaveAttribute', () => { 5 | const { compare, negativeCompare } = toHaveAttribute(); 6 | const { queryByTestId } = render(` 7 | 10 | 11 | `); 12 | const button = queryByTestId('button'); 13 | const svgElement = queryByTestId('svg-element'); 14 | 15 | it('positive test cases', () => { 16 | expect(button).toHaveAttribute('disabled'); 17 | expect(button).toHaveAttribute('type'); 18 | expect(button).toHaveAttribute('type', 'submit'); 19 | expect(button).not.toHaveAttribute('type', 'button'); 20 | expect(button).not.toHaveAttribute('class'); 21 | expect(button).not.toHaveAttribute('width'); 22 | expect(svgElement).toHaveAttribute('width'); 23 | expect(svgElement).toHaveAttribute('width', '12'); 24 | }); 25 | 26 | it('negative test cases', () => { 27 | const { message: buttonDisabledMessage, pass: buttonDisabledPass } = negativeCompare(button, 'disabled'); 28 | const { message: buttonTypeMessage, pass: buttonTypePass } = negativeCompare(button, 'type'); 29 | const { message: buttonClassMessage, pass: buttonClassPass } = compare(button, 'class'); 30 | 31 | expect(buttonDisabledPass).toBeFalse(); 32 | expect(buttonDisabledMessage).toMatch(/Expected the value.*not to be.*but received.*/); 33 | 34 | expect(buttonTypePass).toBeFalse(); 35 | expect(buttonTypeMessage).toMatch(/Expected the value.*not to be.*but received.*/); 36 | 37 | expect(buttonClassPass).toBeFalse(); 38 | expect(buttonClassMessage).toMatch(/Expected the value.*to be.*but received.*/); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { toHaveAttribute } from './toHaveAttribute'; 2 | import { toHaveTextContent } from './toHaveTextContent'; 3 | import { toHaveClassName } from './toHaveClassName'; 4 | import { toBeChecked } from './toBeChecked'; 5 | import { toBeEmptyDOMElement } from './toBeEmptyDOMElement'; 6 | import { toContainHTML } from './toContainHTML'; 7 | import { toHaveFocus } from './toHaveFocus'; 8 | import { toBeDisabled, toBeEnabled } from './toBeDisabled'; 9 | import { toHaveAccessibleDescription } from './toHaveAccessibleDescription'; 10 | import { toHaveAccessibleName } from './toHaveAccessibleName'; 11 | import { toHaveDescription } from './toHaveDescription'; 12 | import { toHaveValue } from './toHaveValue'; 13 | import { toHaveFormValues } from './toHaveFormValues'; 14 | import { toHaveErrorMessage } from './toHaveErrorMessage'; 15 | import { toContainElement } from './toContainElement'; 16 | import { toBeRequired } from './toBeRequired'; 17 | import { toBeInvalid, toBeValid } from './toBeInvalid'; 18 | import { toHaveDisplayValue } from './toHaveDisplayValue'; 19 | import { toBePartiallyChecked } from './toBePartiallyChecked'; 20 | import { toBeInTheDocument } from './toBeInTheDocument'; 21 | import { toBeVisible } from './toBeVisible'; 22 | import { toHaveStyle } from './toHaveStyle'; 23 | 24 | const JasmineDOM = { 25 | toHaveAttribute, 26 | toHaveTextContent, 27 | toHaveClassName, 28 | toBeChecked, 29 | toBeEmptyDOMElement, 30 | toContainHTML, 31 | toHaveAccessibleDescription, 32 | toHaveAccessibleName, 33 | toHaveFocus, 34 | toBeDisabled, 35 | toBeEnabled, 36 | toHaveDescription, 37 | toHaveValue, 38 | toHaveFormValues, 39 | toHaveErrorMessage, 40 | toContainElement, 41 | toBeRequired, 42 | toBeInvalid, 43 | toBeValid, 44 | toHaveDisplayValue, 45 | toBePartiallyChecked, 46 | toBeInTheDocument, 47 | toBeVisible, 48 | toHaveStyle, 49 | }; 50 | 51 | export default JasmineDOM; 52 | -------------------------------------------------------------------------------- /src/toHaveAccessibleDescription.js: -------------------------------------------------------------------------------- 1 | import { computeAccessibleDescription } from 'dom-accessibility-api'; 2 | import { checkHtmlElement } from './utils'; 3 | import { printSuccess, printSecSuccess, printSecError, printError } from './printers'; 4 | 5 | /** 6 | * @param {jasmine.MatchersUtil} matchersUtil 7 | */ 8 | export function toHaveAccessibleDescription(matchersUtil) { 9 | return { 10 | /** 11 | * @param {Element} htmlElement 12 | * @param {RegExp | string | jasmine.AsymmetricMatcher} expectedAccessibleDescription 13 | */ 14 | compare: function (htmlElement, expectedAccessibleDescription) { 15 | checkHtmlElement(htmlElement); 16 | 17 | const actualAccessibleDescription = computeAccessibleDescription(htmlElement); 18 | const missingExpectedValue = arguments.length === 1; 19 | 20 | let pass = false; 21 | if (missingExpectedValue) { 22 | // When called without an expected value we only want to validate that the element has an 23 | // accessible description, whatever it may be. 24 | pass = actualAccessibleDescription !== ''; 25 | } else { 26 | pass = 27 | expectedAccessibleDescription instanceof RegExp 28 | ? expectedAccessibleDescription.test(actualAccessibleDescription) 29 | : matchersUtil.equals(actualAccessibleDescription, expectedAccessibleDescription); 30 | } 31 | 32 | return { 33 | pass, 34 | message: pass 35 | ? `${printSuccess('PASSED')} ${printSecSuccess( 36 | `Expected element to have accessible description:\n${printSuccess( 37 | String(expectedAccessibleDescription) 38 | )}\nReceived:\n${printSuccess(actualAccessibleDescription)}` 39 | )}` 40 | : `${printError('FAILED')} ${printSecError( 41 | `Expected element to have accessible description:\n${printError( 42 | String(expectedAccessibleDescription) 43 | )}\nReceived:\n${printSuccess(actualAccessibleDescription)}` 44 | )}`, 45 | }; 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /.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/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'beta' 8 | - 'alpha' 9 | - '!all-contributors/**' 10 | pull_request: {} 11 | jobs: 12 | main: 13 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 14 | strategy: 15 | matrix: 16 | node: [12, 14, 16] 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: ⬇️ Checkout repo 20 | uses: actions/checkout@v3 21 | 22 | - name: ⎔ Setup node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node }} 26 | 27 | - name: 📥 Download deps 28 | uses: bahmutov/npm-install@v1 29 | with: 30 | useLockFile: false 31 | 32 | - name: ▶️ Run validate script 33 | run: npm run validate 34 | 35 | - name: ⬆️ Upload coverage report 36 | uses: codecov/codecov-action@v3 37 | 38 | release: 39 | needs: main 40 | runs-on: ubuntu-latest 41 | if: ${{ github.repository == 'testing-library/jasmine-dom' && 42 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 43 | github.ref) && github.event_name == 'push' }} 44 | steps: 45 | - name: ⬇️ Checkout repo 46 | uses: actions/checkout@v3 47 | 48 | - name: ⎔ Setup node 49 | uses: actions/setup-node@v3 50 | with: 51 | node-version: 16 52 | 53 | - name: 📥 Download deps 54 | uses: bahmutov/npm-install@v1 55 | with: 56 | useLockFile: false 57 | 58 | - name: 🏗 Run build script 59 | run: npm run build 60 | 61 | - name: 🚀 Release 62 | uses: cycjimmy/semantic-release-action@v3 63 | with: 64 | semantic_version: 17 65 | branches: | 66 | [ 67 | '+([0-9])?(.{+([0-9]),x}).x', 68 | 'main', 69 | {name: 'beta', prerelease: true}, 70 | {name: 'alpha', prerelease: true} 71 | ] 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@testing-library/jasmine-dom", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Custom Jasmine matchers for testing DOM elements", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "kcd-scripts build", 8 | "lint": "kcd-scripts lint", 9 | "lint:fix": "kcd-scripts lint --fix", 10 | "lint:watch": "onchange \"src/**/*.js\" \"tests/**/*.js\" -- npm run lint:fix", 11 | "test": "nyc --reporter=text --reporter=lcov jasmine --config=jasmine.json", 12 | "test:watch": "onchange \"src/**/*.js\" \"src/__tests__/**/*.js\" -- npm run test", 13 | "validate": "npm run lint && npm run test && npm run build", 14 | "setup": "npm install && npm run validate" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "keywords": [ 20 | "testing", 21 | "dom", 22 | "jasmine", 23 | "matchers", 24 | "jsdom" 25 | ], 26 | "author": "Brian Alexis Michel (https://github.com/brrianalexis)", 27 | "license": "MIT", 28 | "dependencies": { 29 | "aria-query": "^5.1.3", 30 | "chalk": "^4.1.0", 31 | "css": "^3.0.0", 32 | "css.escape": "^1.5.1", 33 | "dom-accessibility-api": "^0.5.16", 34 | "lodash": "^4.17.21" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.19.3", 38 | "@babel/core": "^7.20.5", 39 | "@babel/preset-env": "^7.20.2", 40 | "@babel/register": "^7.18.9", 41 | "@types/jasmine": "^4.3.1", 42 | "babel-plugin-dynamic-import-node": "^2.3.3", 43 | "babel-plugin-istanbul": "^6.1.1", 44 | "eslint": "^8.29.0", 45 | "eslint-config-prettier": "^8.5.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "jasmine": "^4.5.0", 48 | "jasmine-reporters": "^2.5.2", 49 | "jasmine-spec-reporter": "^7.0.0", 50 | "jsdom": "^16.2.1", 51 | "kcd-scripts": "^12.3.0", 52 | "nyc": "^15.1.0", 53 | "onchange": "^7.1.0", 54 | "prettier": "^2.8.1" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/testing-library/jasmine-dom" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/testing-library/jasmine-dom/issues" 62 | }, 63 | "homepage": "https://github.com/testing-library/jasmine-dom#readme", 64 | "volta": { 65 | "node": "16.18.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/toHaveAttribute.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement } from './utils'; 2 | import { printSecSuccess, printSuccess, printSecError, printError } from './printers'; 3 | 4 | export function toHaveAttribute(util) { 5 | return { 6 | compare: function (htmlElement, name, expectedValue) { 7 | checkHtmlElement(htmlElement); 8 | let result = {}; 9 | const isExpectedValuePresent = expectedValue !== undefined; 10 | const hasAttribute = htmlElement.hasAttribute(name); 11 | const receivedValue = htmlElement.getAttribute(name); 12 | result.pass = isExpectedValuePresent ? hasAttribute && util.equals(receivedValue, expectedValue) : hasAttribute; 13 | result.message = result.pass 14 | ? `${printSuccess('PASSED')} ${printSecSuccess( 15 | `Expected the value of the received attribute ${printSuccess(`'${name}'`)} to be ${printSuccess( 16 | `'${expectedValue}'` 17 | )}.` 18 | )}` 19 | : `${printError('FAILED')} ${printSecError( 20 | `Expected the value of the received attribute ${printError(`'${name}'`)} to be ${printError( 21 | `'${expectedValue}'` 22 | )}, but received ${printError(`'${receivedValue}'`)}.` 23 | )}`; 24 | return result; 25 | }, 26 | negativeCompare: function (htmlElement, name, expectedValue) { 27 | checkHtmlElement(htmlElement); 28 | let result = {}; 29 | const isExpectedValuePresent = expectedValue !== undefined; 30 | const hasAttribute = htmlElement.hasAttribute(name); 31 | const receivedValue = htmlElement.getAttribute(name); 32 | result.pass = isExpectedValuePresent ? hasAttribute && !util.equals(receivedValue, expectedValue) : !hasAttribute; 33 | result.message = result.pass 34 | ? `${printSuccess('PASSED')} ${printSecSuccess( 35 | `Expected the value of the received attribute ${printSuccess(`'${name}'`)} not to be ${printSuccess( 36 | `'${expectedValue}'` 37 | )}.` 38 | )}` 39 | : `${printError('FAILED')} ${printSecError( 40 | `Expected the value of the received attribute ${printError(`'${name}'`)} not to be ${printError( 41 | `'${expectedValue}'` 42 | )}, but received ${printError(`'${receivedValue}'`)}.` 43 | )}`; 44 | return result; 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/toContainHTML.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/testing-library/jest-dom/blob/main/src/__tests__/to-contain-html.js 3 | */ 4 | 5 | import { checkHtmlElement } from './utils'; 6 | import { printSuccess, printSecSuccess, printSecError, printError, printSecWarning, printWarning } from './printers'; 7 | 8 | function getNormalizedHtml(container, htmlText) { 9 | const div = container.ownerDocument.createElement('div'); 10 | div.innerHTML = htmlText; 11 | return div.innerHTML; 12 | } 13 | 14 | export function toContainHTML() { 15 | return { 16 | compare(htmlElement, htmlText) { 17 | checkHtmlElement(htmlElement); 18 | 19 | if (typeof htmlText !== 'string') { 20 | throw new Error( 21 | printSecWarning( 22 | `${printError('FAILED')}.toContainHTML() expects a string value, got ${printWarning(htmlText)}` 23 | ) 24 | ); 25 | } 26 | 27 | const pass = htmlElement.outerHTML.includes(getNormalizedHtml(htmlElement, htmlText)); 28 | 29 | return { 30 | pass, 31 | message: pass 32 | ? `${printSuccess('PASSED')} ${printSecSuccess( 33 | `Expected: ${printSuccess(`'${htmlText}'`)}. Received: ${printSuccess(htmlElement.outerHTML)}` 34 | )}` 35 | : `${printError('FAILED')} ${printSecError( 36 | `Expected: ${printError(`'${htmlText}'`)}. Received: ${printSuccess(htmlElement.outerHTML)}` 37 | )}`, 38 | }; 39 | }, 40 | 41 | negativeCompare(htmlElement, htmlText) { 42 | checkHtmlElement(htmlElement); 43 | 44 | if (typeof htmlText !== 'string') { 45 | throw new Error( 46 | printSecWarning( 47 | `${printError('FAILED')}.not.toContainHTML() expects a string value, got ${printWarning(htmlText)}` 48 | ) 49 | ); 50 | } 51 | 52 | const pass = !htmlElement.outerHTML.includes(getNormalizedHtml(htmlElement, htmlText)); 53 | 54 | return { 55 | pass, 56 | message: pass 57 | ? `${printSuccess('PASSED')} ${printSecSuccess( 58 | `Expected: ${printError(`'${htmlText}'`)}. Received: ${printSuccess(htmlElement.outerHTML)}` 59 | )}` 60 | : `${printError('FAILED')} ${printSecError( 61 | `Expected: ${printSuccess(`'${htmlText}'`)}. Received: ${printSuccess(htmlElement.outerHTML)}` 62 | )}`, 63 | }; 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "kentcdodds", 10 | "name": "Kent C. Dodds", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 12 | "profile": "https://kentcdodds.com", 13 | "contributions": [ 14 | "infra" 15 | ] 16 | }, 17 | { 18 | "login": "brrianalexis", 19 | "name": "Brian Alexis", 20 | "avatar_url": "https://avatars2.githubusercontent.com/u/51463930?v=4", 21 | "profile": "https://github.com/brrianalexis", 22 | "contributions": [ 23 | "ideas", 24 | "code", 25 | "doc", 26 | "test" 27 | ] 28 | }, 29 | { 30 | "login": "IanGrainger", 31 | "name": "IanGrainger", 32 | "avatar_url": "https://avatars0.githubusercontent.com/u/559336?v=4", 33 | "profile": "https://github.com/IanGrainger", 34 | "contributions": [ 35 | "code" 36 | ] 37 | }, 38 | { 39 | "login": "MichaelDeBoey", 40 | "name": "Michaël De Boey", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/6643991?v=4", 42 | "profile": "https://michaeldeboey.be", 43 | "contributions": [ 44 | "infra" 45 | ] 46 | }, 47 | { 48 | "login": "nickmccurdy", 49 | "name": "Nick McCurdy", 50 | "avatar_url": "https://avatars.githubusercontent.com/u/927220?v=4", 51 | "profile": "https://nickmccurdy.com/", 52 | "contributions": [ 53 | "infra" 54 | ] 55 | }, 56 | { 57 | "login": "oriSomething", 58 | "name": "Ori Livni", 59 | "avatar_url": "https://avatars.githubusercontent.com/u/2685242?v=4", 60 | "profile": "http://www.orilivni.com", 61 | "contributions": [ 62 | "code", 63 | "test" 64 | ] 65 | }, 66 | { 67 | "login": "sebastian-altamirano", 68 | "name": "Sebastián Altamirano", 69 | "avatar_url": "https://avatars.githubusercontent.com/u/38230545?v=4", 70 | "profile": "https://github.com/sebastian-altamirano", 71 | "contributions": [ 72 | "doc" 73 | ] 74 | } 75 | ], 76 | "contributorsPerLine": 7, 77 | "projectName": "jasmine-dom", 78 | "projectOwner": "testing-library", 79 | "repoType": "github", 80 | "repoHost": "https://github.com", 81 | "skipCi": true, 82 | "commitConvention": "angular", 83 | "commitType": "docs" 84 | } 85 | -------------------------------------------------------------------------------- /src/__tests__/toBeInTheDocument.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import document from './helpers/jsdom'; 3 | import { toBeInTheDocument } from '../toBeInTheDocument'; 4 | import { HtmlElementTypeError } from '../utils'; 5 | 6 | describe('.toBeInTheDocument()', () => { 7 | const { compare, negativeCompare } = toBeInTheDocument(); 8 | const { container, queryByTestId } = render(` 9 | HTML element 10 | SVG element 11 | `); 12 | document.body.innerHTML = ` 13 | Html Element 14 | `; 15 | 16 | const detachedElement = document.createElement('div'); 17 | const notAnElement = { whatever: 'clearly not an element' }; 18 | const undefinedElement = undefined; 19 | const nullElement = null; 20 | 21 | it('positive compare', () => { 22 | const { message: detachedMessage, pass: detachedPass } = compare(detachedElement); 23 | const { message: nullMessage, pass: nullPass } = compare(nullElement); 24 | 25 | document.body.appendChild(container); 26 | 27 | expect(detachedPass).toBeFalse(); 28 | expect(detachedMessage).toMatch(/The.*div.*element provided.*could not be found in the document.*\./); 29 | 30 | expect(queryByTestId('html-element')).toBeInTheDocument(); 31 | expect(queryByTestId('svg-element')).toBeInTheDocument(); 32 | 33 | expect(() => expect(notAnElement).toBeInTheDocument()).toThrowError(); 34 | expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError(); 35 | 36 | expect(nullPass).toBeFalse(); 37 | expect(nullMessage).toMatch(/The.*null.*element provided.*could not be found in the document.*\./); 38 | }); 39 | 40 | it('negative compare', () => { 41 | const { message: htmlMessage, pass: htmlPass } = negativeCompare(queryByTestId('html-element')); 42 | const { message: svgMessage, pass: svgPass } = negativeCompare(queryByTestId('svg-element')); 43 | 44 | expect(htmlPass).toBeFalse(); 45 | expect(htmlMessage).toMatch(/Expected the document not to contain the provided.*span.*element\./); 46 | 47 | expect(svgPass).toBeFalse(); 48 | expect(svgMessage).toMatch(/Expected the document not to contain the provided.*svg.*element\./); 49 | 50 | expect(detachedElement).not.toBeInTheDocument(); 51 | expect(nullElement).not.toBeInTheDocument(); 52 | 53 | expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError(HtmlElementTypeError); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import document from './helpers/jsdom'; 2 | import { checkHtmlElement, toSentence } from '../utils'; 3 | 4 | describe('Utils file', () => { 5 | describe('checkHtmlElement()', () => { 6 | it("doesn't throw for a correct HTML element", () => { 7 | expect(() => { 8 | const htmlElement = document.createElement('p'); 9 | checkHtmlElement(htmlElement, () => {}, {}); 10 | }).not.toThrow(); 11 | }); 12 | 13 | it("doesn't throw for a correct SVG element", () => { 14 | expect(() => { 15 | const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); 16 | checkHtmlElement(svgElement, () => {}, {}); 17 | }).not.toThrow(); 18 | }); 19 | 20 | it("doesn't throw for document body", () => { 21 | expect(() => { 22 | checkHtmlElement(document.body, () => {}, {}); 23 | }).not.toThrow(); 24 | }); 25 | 26 | it('throws for undefined', () => { 27 | expect(() => { 28 | checkHtmlElement(undefined, () => {}, {}); 29 | }).toThrow(); 30 | }); 31 | 32 | it('throws for document', () => { 33 | expect(() => { 34 | checkHtmlElement(document, () => {}, {}); 35 | }).toThrow(); 36 | }); 37 | 38 | it('throws for function', () => { 39 | expect(() => { 40 | checkHtmlElement( 41 | () => {}, 42 | () => {}, 43 | {} 44 | ); 45 | }).toThrow(); 46 | }); 47 | 48 | it('throws for almost element-like objects', () => { 49 | class FakeObject {} 50 | expect(() => { 51 | checkHtmlElement( 52 | { 53 | ownerDocument: { 54 | defaultView: { HTMLElement: FakeObject, SVGElement: FakeObject }, 55 | }, 56 | }, 57 | () => {}, 58 | {} 59 | ); 60 | }).toThrow(); 61 | }); 62 | }); 63 | 64 | describe('toSentence', () => { 65 | it('turns array into string of comma separated list with default last word connector', () => { 66 | expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three'); 67 | }); 68 | 69 | it('supports custom word connector', () => { 70 | expect(toSentence(['one', 'two', 'three'], { wordConnector: '; ' })).toBe('one; two and three'); 71 | }); 72 | 73 | it('supports custom last word connector', () => { 74 | expect(toSentence(['one', 'two', 'three'], { lastWordConnector: ' or ' })).toBe('one, two or three'); 75 | }); 76 | 77 | it('turns one element array into string containing first element', () => { 78 | expect(toSentence(['one'])).toBe('one'); 79 | }); 80 | 81 | it('turns empty array into empty string', () => { 82 | expect(toSentence([])).toBe(''); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/toHaveTextContent.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, normalize, matches } from './utils'; 2 | import { printError, printSuccess, printSecError, printSecWarning, printSecSuccess } from './printers'; 3 | 4 | export function toHaveTextContent() { 5 | return { 6 | compare: function ( 7 | htmlElement, 8 | checkWith, 9 | options = { 10 | normalizeWhitespace: true, 11 | } 12 | ) { 13 | checkHtmlElement(htmlElement); 14 | let result = {}; 15 | const textContent = options.normalizeWhitespace 16 | ? normalize(htmlElement.textContent) 17 | : htmlElement.textContent.replace(/\u00a0/g, ' '); 18 | const checkingWithEmptyString = textContent !== '' && checkWith === ''; 19 | const providedArgs = checkWith !== undefined; 20 | result.pass = !checkingWithEmptyString && providedArgs && matches(textContent, checkWith); 21 | result.message = 22 | checkingWithEmptyString || !providedArgs 23 | ? `${printError('FAILED')} ${printSecWarning( 24 | `Checking with an empty string will always match. Try using ${printSuccess('.toBeEmptyDOMElement()')}.` 25 | )}` 26 | : result.pass 27 | ? `${printSuccess('PASSED')} ${printSecSuccess( 28 | `Expected ${printSuccess(`'${htmlElement.textContent}'`)} to match ${printSuccess(`'${checkWith}'`)}.` 29 | )}` 30 | : `${printError('FAILED')} ${printSecError( 31 | `Expected ${printError(`'${htmlElement.textContent}'`)} to match ${printError(`'${checkWith}'`)}.` 32 | )}`; 33 | return result; 34 | }, 35 | negativeCompare: function ( 36 | htmlElement, 37 | checkWith, 38 | options = { 39 | normalizeWhitespace: true, 40 | } 41 | ) { 42 | checkHtmlElement(htmlElement); 43 | let result = {}; 44 | const textContent = options.normalizeWhitespace 45 | ? normalize(htmlElement.textContent) 46 | : htmlElement.textContent.replace(/\u00a0/g, ' '); 47 | const checkingWithEmptyString = textContent !== '' && checkWith === ''; 48 | const providedArgs = checkWith !== undefined; 49 | result.pass = !checkingWithEmptyString && providedArgs && !matches(textContent, checkWith); 50 | result.message = 51 | checkingWithEmptyString || !providedArgs 52 | ? `${printError('FAILED')} ${printSecWarning( 53 | `Checking with an empty string will always match. Try using ${printSuccess('.toBeEmptyDOMElement()')}.` 54 | )}` 55 | : result.pass 56 | ? `${printSuccess('PASSED')} ${printSecSuccess( 57 | `Expected ${printSuccess(`'${htmlElement.textContent}'`)} not to match ${printSuccess(`'${checkWith}'`)}.` 58 | )}` 59 | : `${printError('FAILED')} ${printSecError( 60 | `Expected ${printError(`'${htmlElement.textContent}'`)} not to match ${printError(`'${checkWith}'`)}.` 61 | )}`; 62 | return result; 63 | }, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/toHaveErrorMessage.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, normalize } from './utils'; 2 | import { printSecSuccess, printSuccess, printSecError, printError } from './printers'; 3 | 4 | /** 5 | * @see aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage 6 | * @param {jasmine.MatchersUtil} matchersUtil 7 | */ 8 | export function toHaveErrorMessage(matchersUtil) { 9 | return { 10 | compare: function (htmlElement, checkWith) { 11 | checkHtmlElement(htmlElement); 12 | 13 | if (!htmlElement.hasAttribute('aria-invalid') || htmlElement.getAttribute('aria-invalid') === 'false') { 14 | return { 15 | pass: false, 16 | message: pass 17 | ? `${printSuccess('PASSED')} ${printSecSuccess( 18 | `Expected the element to have invalid state indicated by\n${printSuccess( 19 | `aria-invalid="true"` 20 | )}\nReceived\n${printSuccess( 21 | htmlElement.hasAttribute('aria-invalid') 22 | ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"` 23 | : `''` 24 | )}` 25 | )}` 26 | : `${printError('FAILED')} ${printSecError( 27 | `Expected the element to have invalid state indicated by\n${printError( 28 | `aria-invalid="true"` 29 | )}\nReceived\n${printSuccess( 30 | htmlElement.hasAttribute('aria-invalid') 31 | ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"` 32 | : `''` 33 | )}` 34 | )}`, 35 | }; 36 | } 37 | 38 | const expectsErrorMessage = checkWith !== undefined; 39 | 40 | const errormessageIDRaw = htmlElement.getAttribute('aria-errormessage') || ''; 41 | const errormessageIDs = errormessageIDRaw.split(/\s+/).filter(Boolean); 42 | 43 | let errormessage = ''; 44 | if (errormessageIDs.length > 0) { 45 | const document = htmlElement.ownerDocument; 46 | 47 | const errormessageEls = errormessageIDs 48 | .map(errormessageID => document.getElementById(errormessageID)) 49 | .filter(Boolean); 50 | 51 | errormessage = normalize(errormessageEls.map(el => el.textContent).join(' ')); 52 | } 53 | 54 | const pass = expectsErrorMessage 55 | ? checkWith instanceof RegExp 56 | ? checkWith.test(errormessage) 57 | : matchersUtil.equals(errormessage, checkWith) 58 | : Boolean(errormessage); 59 | 60 | return { 61 | pass, 62 | message: pass 63 | ? `${printSuccess('PASSED')} ${printSecSuccess( 64 | `Expected the element to have error message\n${printSuccess(checkWith)}\nReceived\n${printSuccess( 65 | errormessage 66 | )}` 67 | )}` 68 | : `${printError('FAILED')} ${printSecError( 69 | `Expected the element to have error message\n${printError(checkWith)}\nReceived\n${printSuccess( 70 | errormessage 71 | )}` 72 | )}`, 73 | }; 74 | }, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/toBePartiallyChecked.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printWarning, printSecWarning, printError, printSecError, printSuccess, printSecSuccess } from './printers'; 3 | 4 | function isValidCheckbox(htmlElement) { 5 | return getTag(htmlElement) === 'input' && htmlElement.type === 'checkbox'; 6 | } 7 | 8 | function isValidAriaElement(htmlElement) { 9 | return htmlElement.getAttribute('role') === 'checkbox'; 10 | } 11 | 12 | function isPartiallyChecked(htmlElement) { 13 | const isAriaMixed = htmlElement.getAttribute('aria-checked') === 'mixed'; 14 | if (isValidCheckbox(htmlElement)) { 15 | return htmlElement.indeterminate || isAriaMixed; 16 | } 17 | return isAriaMixed; 18 | } 19 | 20 | export function toBePartiallyChecked() { 21 | return { 22 | compare: function (htmlElement) { 23 | checkHtmlElement(htmlElement); 24 | let result = {}; 25 | if (!isValidCheckbox(htmlElement) && !isValidAriaElement(htmlElement)) { 26 | result.pass = false; 27 | result.message = `${printError('FAILED')} ${printSecWarning( 28 | `Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with ${printWarning( 29 | '.toBePartiallyChecked()' 30 | )}. Use ${printSuccess('.toHaveValue()')} instead.` 31 | )}`; 32 | return result; 33 | } 34 | result.pass = isPartiallyChecked(htmlElement); 35 | result.message = `${ 36 | result.pass 37 | ? `${printSuccess('PASSED')} ${printSecSuccess( 38 | `Expected the element ${printSuccess(getTag(htmlElement))} to be partially checked, and it ${printSuccess( 39 | 'is partially checked' 40 | )}.` 41 | )}` 42 | : `${printError('FAILED')} ${printSecError( 43 | `Expected the element ${printError(getTag(htmlElement))} to be partially checked, and it ${printError( 44 | "isn't partially checked" 45 | )}.` 46 | )}` 47 | }`; 48 | return result; 49 | }, 50 | negativeCompare: function (htmlElement) { 51 | checkHtmlElement(htmlElement); 52 | let result = {}; 53 | if (!isValidCheckbox(htmlElement) && !isValidAriaElement(htmlElement)) { 54 | result.pass = false; 55 | result.message = `${printError('FAILED')} ${printSecWarning( 56 | `Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with ${printWarning( 57 | '.toBePartiallyChecked()' 58 | )}. Use ${printSuccess('.toHaveValue()')} instead.` 59 | )}`; 60 | return result; 61 | } 62 | result.pass = !isPartiallyChecked(htmlElement); 63 | result.message = `${ 64 | result.pass 65 | ? `${printSuccess('PASSED')} ${printSecSuccess( 66 | `Expected the element ${printSuccess( 67 | getTag(htmlElement) 68 | )} not to be partially checked, and it ${printSuccess("isn't partially checked")}.` 69 | )}` 70 | : `${printError('FAILED')} ${printSecError( 71 | `Expected the element ${printError(getTag(htmlElement))} not to be partially checked, and it ${printError( 72 | 'is partially checked' 73 | )}.` 74 | )}` 75 | }`; 76 | return result; 77 | }, 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/toBeVisible.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSuccess, printSecSuccess, printError, printSecError, printSecWarning, printWarning } from './printers'; 3 | 4 | function isStyleVisible(htmlElement) { 5 | const { getComputedStyle } = htmlElement.ownerDocument.defaultView; 6 | const { display, visibility, opacity } = getComputedStyle(htmlElement); 7 | return display !== 'none' && visibility !== 'hidden' && visibility !== 'collapse' && opacity !== '0' && opacity !== 0; 8 | } 9 | 10 | function isAttributeVisible(htmlElement, previousElement) { 11 | return ( 12 | !htmlElement.hasAttribute('hidden') && 13 | (htmlElement.nodeName === 'DETAILS' && previousElement.nodeName !== 'SUMMARY' 14 | ? htmlElement.hasAttribute('open') 15 | : true) 16 | ); 17 | } 18 | 19 | function isElementVisible(htmlElement, previousElement) { 20 | return ( 21 | isStyleVisible(htmlElement) && 22 | isAttributeVisible(htmlElement, previousElement) && 23 | (!htmlElement.parentElement || isElementVisible(htmlElement.parentElement, htmlElement)) 24 | ); 25 | } 26 | 27 | export function toBeVisible() { 28 | return { 29 | compare: function (htmlElement) { 30 | checkHtmlElement(htmlElement); 31 | let result = {}; 32 | const isVisible = isElementVisible(htmlElement); 33 | result.pass = isVisible; 34 | result.message = `${ 35 | result.pass 36 | ? `${printSuccess('PASSED')} ${printSecSuccess( 37 | `Expected the provided ${printSuccess(getTag(htmlElement))} element to be visible and it ${printSuccess( 38 | 'is visible' 39 | )}.` 40 | )}` 41 | : `${printError('FAILED')} ${printSecError( 42 | `Expected the provided ${printError(getTag(htmlElement))} element to be visible and it ${printError( 43 | "isn't visible" 44 | )}.` 45 | )} \n🤔 ${printSecWarning( 46 | `Take a look at the ${printWarning('display')}, ${printWarning('visibility')} and ${printWarning( 47 | 'opacity' 48 | )} CSS properties of the provided element and the elements up on to the top of the DOM tree.` 49 | )}` 50 | }`; 51 | return result; 52 | }, 53 | negativeCompare: function (htmlElement) { 54 | checkHtmlElement(htmlElement); 55 | let result = {}; 56 | const isVisible = isElementVisible(htmlElement); 57 | result.pass = !isVisible; 58 | result.message = `${ 59 | result.pass 60 | ? `${printSuccess('PASSED')} ${printSecSuccess( 61 | `Expected the provided ${printSuccess( 62 | getTag(htmlElement) 63 | )} element not to be visible and it ${printSuccess("isn't visible")}.` 64 | )}` 65 | : `${printError('FAILED')} ${printSecError( 66 | `Expected the provided ${printError(getTag(htmlElement))} element not to be visible and it ${printError( 67 | 'is visible' 68 | )}.` 69 | )} \n🤔 ${printSecWarning( 70 | `Take a look at the ${printWarning('display')}, ${printWarning('visibility')} and ${printWarning( 71 | 'opacity' 72 | )} CSS properties of the provided element and the elements up on to the top of the DOM tree.` 73 | )}` 74 | }`; 75 | return result; 76 | }, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/toBeRequired.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSuccess, printSecSuccess, printError, printSecError } from './printers'; 3 | 4 | const REQUIRED_FORM_TAGS = ['select', 'textarea']; 5 | const REQUIRED_ARIA_FORM_TAGS = ['input', 'select', 'textarea']; 6 | const REQUIRED_UNSUPPORTED_INPUT_TYPES = ['color', 'hidden', 'range', 'submit', 'image', 'reset']; 7 | const REQUIRED_SUPPORTED_ARIA_ROLES = ['combobox', 'gridcell', 'radiogroup', 'spinbutton', 'tree']; 8 | 9 | function isRequiredOnSupportedInput(htmlElement) { 10 | return ( 11 | getTag(htmlElement) === 'input' && 12 | htmlElement.hasAttribute('required') && 13 | ((htmlElement.hasAttribute('type') && 14 | !REQUIRED_UNSUPPORTED_INPUT_TYPES.includes(htmlElement.getAttribute('type'))) || 15 | !htmlElement.hasAttribute('type')) 16 | ); 17 | } 18 | 19 | function isRequiredOnFormTagsExceptInput(htmlElement) { 20 | return REQUIRED_FORM_TAGS.includes(getTag(htmlElement)) && htmlElement.hasAttribute('required'); 21 | } 22 | 23 | function isElementRequiredByARIA(htmlElement) { 24 | return ( 25 | htmlElement.hasAttribute('aria-required') && 26 | htmlElement.getAttribute('aria-required') === 'true' && 27 | (REQUIRED_ARIA_FORM_TAGS.includes(getTag(htmlElement)) || 28 | (htmlElement.hasAttribute('role') && REQUIRED_SUPPORTED_ARIA_ROLES.includes(htmlElement.getAttribute('role')))) 29 | ); 30 | } 31 | 32 | export function toBeRequired() { 33 | return { 34 | compare: function (htmlElement) { 35 | checkHtmlElement(htmlElement); 36 | let result = {}; 37 | const isRequired = 38 | isRequiredOnFormTagsExceptInput(htmlElement) || 39 | isRequiredOnSupportedInput(htmlElement) || 40 | isElementRequiredByARIA(htmlElement); 41 | result.pass = isRequired; 42 | result.message = `${ 43 | result.pass 44 | ? `${printSuccess('PASSED')} ${printSecSuccess( 45 | `Expected the provided ${printSuccess(getTag(htmlElement))} element to be required, and it ${printSuccess( 46 | 'is required' 47 | )}.` 48 | )}` 49 | : `${printError('FAILED')} ${printSecError( 50 | `Expected the provided ${printError(getTag(htmlElement))} element to be required, and it ${printError( 51 | "isn't required" 52 | )}.` 53 | )}` 54 | }`; 55 | return result; 56 | }, 57 | negativeCompare: function (htmlElement) { 58 | checkHtmlElement(htmlElement); 59 | let result = {}; 60 | const isRequired = 61 | isRequiredOnFormTagsExceptInput(htmlElement) || 62 | isRequiredOnSupportedInput(htmlElement) || 63 | isElementRequiredByARIA(htmlElement); 64 | result.pass = !isRequired; 65 | result.message = `${ 66 | result.pass 67 | ? `${printSuccess('PASSED')} ${printSecSuccess( 68 | `Expected the provided ${printSuccess( 69 | getTag(htmlElement) 70 | )} element not to be required, and it ${printSuccess("isn't required")}.` 71 | )}` 72 | : `${printError('FAILED')} ${printSecError( 73 | `Expected the provided ${printError(getTag(htmlElement))} element not to be required, and it ${printError( 74 | 'is required' 75 | )}.` 76 | )}` 77 | }`; 78 | return result; 79 | }, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/toHaveDescription.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, normalize, getTag } from './utils'; 2 | import { printSecSuccess, printSuccess, printSecError, printError } from './printers'; 3 | 4 | export function toHaveDescription(util) { 5 | return { 6 | compare: function (htmlElement, checkWith) { 7 | checkHtmlElement(htmlElement); 8 | let result = {}; 9 | let description = ''; 10 | const expectsDescription = checkWith !== undefined; 11 | const descriptionIDRaw = htmlElement.getAttribute('aria-describedby') || ''; 12 | const descriptionIDs = descriptionIDRaw.split(/\s+/).filter(Boolean); 13 | if (descriptionIDs.length > 0) { 14 | const document = htmlElement.ownerDocument; 15 | const descriptionElements = descriptionIDs 16 | .map(descriptionID => document.getElementById(descriptionID)) 17 | .filter(Boolean); 18 | description = normalize(descriptionElements.map(element => element.textContent).join(' ')); 19 | } 20 | result.pass = expectsDescription 21 | ? checkWith instanceof RegExp 22 | ? checkWith.test(description) 23 | : util.equals(description, checkWith) 24 | : Boolean(description); 25 | checkWith === undefined ? (checkWith = '') : null; 26 | result.message = result.pass 27 | ? `${printSuccess('PASSED')} ${printSecSuccess( 28 | `Expected the ${printSuccess(getTag(htmlElement))} element to have description ${printSuccess( 29 | `'${checkWith}'` 30 | )}. Received ${printSuccess(`'${description}'`)}.` 31 | )}` 32 | : `${printError('FAILED')} ${printSecError( 33 | `Expected the ${printError(getTag(htmlElement))} element to have description ${printError( 34 | `'${checkWith}'` 35 | )}. Received ${printError(`'${description}'`)}.` 36 | )}`; 37 | return result; 38 | }, 39 | negativeCompare: function (htmlElement, checkWith) { 40 | checkHtmlElement(htmlElement); 41 | let result = {}; 42 | let description = ''; 43 | const expectsNotDescription = checkWith !== undefined; 44 | const descriptionIDRaw = htmlElement.getAttribute('aria-describedby') || ''; 45 | const descriptionIDs = descriptionIDRaw.split(/\s+/).filter(Boolean); 46 | if (descriptionIDs.length > 0) { 47 | const document = htmlElement.ownerDocument; 48 | const descriptionElements = descriptionIDs 49 | .map(descriptionID => document.getElementById(descriptionID)) 50 | .filter(Boolean); 51 | description = normalize(descriptionElements.map(element => element.textContent).join(' ')); 52 | } 53 | result.pass = expectsNotDescription 54 | ? checkWith instanceof RegExp 55 | ? !checkWith.test(description) 56 | : !util.equals(description, checkWith) 57 | : !Boolean(description); 58 | checkWith === undefined ? (checkWith = '') : null; 59 | result.message = result.pass 60 | ? `${printSuccess('PASSED')} ${printSecSuccess( 61 | `Expected the ${printSuccess(getTag(htmlElement))} element not to have description ${printSuccess( 62 | `'${checkWith}'` 63 | )}. Received ${printSuccess(`'${description}'`)}.` 64 | )}` 65 | : `${printError('FAILED')} ${printSecError( 66 | `Expected the ${printError(getTag(htmlElement))} element not to have description ${printError( 67 | `'${checkWith}'` 68 | )}. Received ${printError(`'${description}'`)}.` 69 | )}`; 70 | return result; 71 | }, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/__tests__/toBeInvalid.test.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import { render } from './helpers/renderer'; 3 | import { toBeInvalid } from '../toBeInvalid'; 4 | 5 | function getDOMElement(htmlString, selector) { 6 | return new JSDOM(htmlString).window.document.querySelector(selector); 7 | } 8 | 9 | const invalidInputHTML = ``; 10 | const invalidInputNode = getDOMElement(invalidInputHTML, 'input'); 11 | 12 | const invalidFormHTML = `
${invalidInputHTML}
`; 13 | const invalidFormNode = getDOMElement(invalidFormHTML, 'form'); 14 | 15 | describe('.toBeInvalid', () => { 16 | const { compare, negativeCompare } = toBeInvalid(); 17 | it('input', () => { 18 | const { queryByTestId } = render(` 19 |
20 | 21 | 22 | 23 | 24 |
25 | `); 26 | 27 | expect(invalidInputNode).toBeInvalid(); 28 | expect(queryByTestId('aria-invalid')).toBeInvalid(); 29 | expect(queryByTestId('aria-invalid-value')).toBeInvalid(); 30 | expect(queryByTestId('no-aria-invalid')).not.toBeInvalid(); 31 | expect(queryByTestId('aria-invalid-false')).not.toBeInvalid(); 32 | }); 33 | 34 | it('form', () => { 35 | const { queryByTestId } = render(` 36 |
37 | 38 |
39 | `); 40 | 41 | expect(queryByTestId('valid')).not.toBeInvalid(); 42 | expect(invalidFormNode).toBeInvalid(); 43 | }); 44 | 45 | it('other elements', () => { 46 | const { queryByTestId } = render(` 47 |
    48 |
  1. 49 |
  2. 50 |
  3. 51 |
  4. 52 |
53 | `); 54 | 55 | expect(queryByTestId('aria-invalid')).toBeInvalid(); 56 | expect(queryByTestId('aria-invalid-value')).toBeInvalid(); 57 | expect(queryByTestId('valid')).not.toBeInvalid(); 58 | expect(queryByTestId('no-aria-invalid')).not.toBeInvalid(); 59 | expect(queryByTestId('aria-invalid-false')).not.toBeInvalid(); 60 | }); 61 | 62 | it('negative test cases', () => { 63 | const { queryByTestId } = render(` 64 |
65 | 66 | 67 | 68 | 69 |
70 | `); 71 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('no-aria-invalid')); 72 | const { message: negativeMessage, pass: negativePass } = negativeCompare(queryByTestId('aria-invalid')); 73 | 74 | expect(positivePass).toBeFalse(); 75 | expect(positiveMessage).toMatch(/Expected the element.*input.*to be invalid, and it.*isn't invalid.*\./); 76 | expect(negativePass).toBeFalse(); 77 | expect(negativeMessage).toMatch(/Expected the element.*input.*not to be invalid, and it.*is invalid.*\./); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kent+coc@doddsfamily.us. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /src/__tests__/toContainElement.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toContainElement } from '../toContainElement'; 3 | import { HtmlElementTypeError } from '../utils'; 4 | 5 | describe('.toContainElement', () => { 6 | const { compare, negativeCompare } = toContainElement(); 7 | const { queryByTestId } = render(` 8 | 9 | 10 | 11 | 12 | 13 | 14 | `); 15 | 16 | const grandparent = queryByTestId('grandparent'); 17 | const parent = queryByTestId('parent'); 18 | const child = queryByTestId('child'); 19 | const svgElement = queryByTestId('svg-element'); 20 | const unexistentElement = queryByTestId("doesn't-exist"); 21 | const notAnElement = { whatever: 'clearly not an element' }; 22 | 23 | it('positive test cases', () => { 24 | expect(grandparent).toContainElement(parent); 25 | expect(grandparent).toContainElement(child); 26 | expect(grandparent).toContainElement(svgElement); 27 | expect(parent).toContainElement(child); 28 | expect(parent).not.toContainElement(grandparent); 29 | expect(parent).not.toContainElement(svgElement); 30 | expect(child).not.toContainElement(parent); 31 | expect(child).not.toContainElement(grandparent); 32 | expect(child).not.toContainElement(svgElement); 33 | expect(grandparent).not.toContainElement(unexistentElement); 34 | }); 35 | 36 | it('negative test cases', () => { 37 | const { message: parentGrandparentMessage, pass: parentGrandparentPass } = compare(parent, grandparent); 38 | const { message: grandparentUnexistentMessage, pass: grandparentUnexistentPass } = compare( 39 | grandparent, 40 | unexistentElement 41 | ); 42 | const { message: grandparentChildMessage, pass: grandparentChildPass } = negativeCompare(grandparent, child); 43 | const { message: grandparentSvgMessage, pass: grandparentSvgPass } = negativeCompare(grandparent, svgElement); 44 | 45 | expect(parentGrandparentPass).toBeFalse(); 46 | expect(parentGrandparentMessage).toMatch(/Expected the element.*to contain/); 47 | expect(grandparentUnexistentPass).toBeFalse(); 48 | expect(grandparentUnexistentMessage).toMatch(/Expected the element.*to contain/); 49 | expect(grandparentChildPass).toBeFalse(); 50 | expect(grandparentChildMessage).toMatch(/Expected the element.*not to contain/); 51 | expect(grandparentSvgPass).toBeFalse(); 52 | expect(grandparentSvgMessage).toMatch(/Expected the element.*not to contain/); 53 | 54 | expect(() => negativeCompare(unexistentElement, child)).toThrowError(HtmlElementTypeError); 55 | expect(() => compare(unexistentElement, grandparent)).toThrowError(HtmlElementTypeError); 56 | expect(() => compare(unexistentElement, unexistentElement)).toThrowError(HtmlElementTypeError); 57 | expect(() => compare(unexistentElement, notAnElement)).toThrowError(HtmlElementTypeError); 58 | expect(() => compare(notAnElement, unexistentElement)).toThrowError(HtmlElementTypeError); 59 | expect(() => negativeCompare(notAnElement, unexistentElement)).toThrowError(HtmlElementTypeError); 60 | expect(() => compare(notAnElement, grandparent)).toThrowError(HtmlElementTypeError); 61 | expect(() => compare(grandparent, notAnElement)).toThrowError(HtmlElementTypeError); 62 | expect(() => compare(notAnElement, notAnElement)).toThrowError(HtmlElementTypeError); 63 | expect(() => negativeCompare(grandparent, undefined)).toThrowError(HtmlElementTypeError); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /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 merging anything 37 | 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]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 70 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { isEqual, isEqualWith, uniq } from 'lodash'; 2 | import cssEscape from 'css.escape'; 3 | import { printError, printSecError, printWarning, printSecWarning } from './printers'; 4 | 5 | class HtmlElementTypeError extends Error { 6 | constructor(htmlElement) { 7 | super(); 8 | this.message = `${printError('FAILED')} ${printSecWarning( 9 | `Received element must be an HTMLElement or an SVGElement.\nReceived: ${printWarning(htmlElement)}` 10 | )}`; 11 | } 12 | } 13 | 14 | class InvalidCSSError extends Error { 15 | constructor(received) { 16 | super(); 17 | this.message = [received.message, '', printSecError(`Failing CSS:`), printError(received.css)].join('\n'); 18 | } 19 | } 20 | 21 | function checkHasWindow(htmlElement, ...args) { 22 | if (!htmlElement || !htmlElement.ownerDocument || !htmlElement.ownerDocument.defaultView) { 23 | throw new HtmlElementTypeError(htmlElement); 24 | } 25 | } 26 | 27 | function checkHtmlElement(htmlElement, ...args) { 28 | checkHasWindow(htmlElement, ...args); 29 | const window = htmlElement.ownerDocument.defaultView; 30 | if (!(htmlElement instanceof window.HTMLElement) && !(htmlElement instanceof window.SVGElement)) { 31 | throw new HtmlElementTypeError(htmlElement); 32 | } 33 | } 34 | 35 | function normalize(text) { 36 | return text.replace(/\s+/g, ' ').trim(); 37 | } 38 | 39 | function matches(textToMatch, matcher) { 40 | if (matcher instanceof RegExp) { 41 | return matcher.test(textToMatch); 42 | } else { 43 | return textToMatch.includes(String(matcher)); 44 | } 45 | } 46 | 47 | function getTag(htmlElement) { 48 | return htmlElement === null ? null : htmlElement.tagName && htmlElement.tagName.toLowerCase(); 49 | } 50 | 51 | function getInputValue(inputElement) { 52 | switch (inputElement.type) { 53 | case 'number': 54 | return inputElement.value === '' ? null : Number(inputElement.value); 55 | 56 | case 'checkbox': 57 | return inputElement.checked; 58 | 59 | default: 60 | return inputElement.value; 61 | } 62 | } 63 | 64 | function getSelectValue({ multiple, options }) { 65 | const selectedOptions = [...options].filter(option => option.selected); 66 | if (multiple) { 67 | return [...selectedOptions].map(option => option.value); 68 | } 69 | if (selectedOptions.length === 0) { 70 | return undefined; 71 | } 72 | return selectedOptions[0].value; 73 | } 74 | 75 | function getSingleElementValue(htmlElement) { 76 | if (!htmlElement) { 77 | return undefined; 78 | } 79 | 80 | switch (htmlElement.tagName.toLowerCase()) { 81 | case 'input': 82 | return getInputValue(htmlElement); 83 | 84 | case 'select': 85 | return getSelectValue(htmlElement); 86 | 87 | default: 88 | return htmlElement.value; 89 | } 90 | } 91 | 92 | function compareArraysAsSet(a, b) { 93 | if (Array.isArray(a) && Array.isArray(b)) { 94 | return isEqual(new Set(a), new Set(b)); 95 | } 96 | return undefined; 97 | } 98 | 99 | function toSentence(array, { wordConnector = ', ', lastWordConnector = ' and ' } = {}) { 100 | return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join( 101 | array.length > 1 ? lastWordConnector : '' 102 | ); 103 | } 104 | 105 | export { 106 | checkHasWindow, 107 | checkHtmlElement, 108 | HtmlElementTypeError, 109 | InvalidCSSError, 110 | normalize, 111 | matches, 112 | getTag, 113 | getSingleElementValue, 114 | compareArraysAsSet, 115 | isEqualWith, 116 | cssEscape, 117 | uniq, 118 | toSentence, 119 | }; 120 | -------------------------------------------------------------------------------- /src/toBeInvalid.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSuccess, printSecSuccess, printError, printSecError } from './printers'; 3 | 4 | const INVALID_FORM_TAGS = ['form', 'input', 'select', 'textarea']; 5 | 6 | function isElementHavingAriaInvalid(htmlElement) { 7 | return htmlElement.hasAttribute('aria-invalid') && htmlElement.getAttribute('aria-invalid') !== 'false'; 8 | } 9 | 10 | function supportsValidityMethod(htmlElement) { 11 | return INVALID_FORM_TAGS.includes(getTag(htmlElement)); 12 | } 13 | 14 | function isElementInvalid(htmlElement) { 15 | const hasAriaInvalid = isElementHavingAriaInvalid(htmlElement); 16 | if (supportsValidityMethod(htmlElement)) { 17 | return hasAriaInvalid || !htmlElement.checkValidity(); 18 | } else { 19 | return hasAriaInvalid; 20 | } 21 | } 22 | 23 | export function toBeInvalid() { 24 | return { 25 | compare: function (htmlElement) { 26 | checkHtmlElement(htmlElement); 27 | let result = {}; 28 | const isInvalid = isElementInvalid(htmlElement); 29 | result.pass = isInvalid; 30 | result.message = `${ 31 | result.pass 32 | ? `${printSuccess('PASSED')} ${printSecSuccess( 33 | `Expected the element ${printSuccess(getTag(htmlElement))} to be invalid, and it ${printSuccess( 34 | 'is invalid' 35 | )}.` 36 | )}` 37 | : `${printError('FAILED')} ${printSecError( 38 | `Expected the element ${printError(getTag(htmlElement))} to be invalid, and it ${printError( 39 | "isn't invalid" 40 | )}.` 41 | )}` 42 | }`; 43 | return result; 44 | }, 45 | negativeCompare: function (htmlElement) { 46 | checkHtmlElement(htmlElement); 47 | let result = {}; 48 | const isValid = !isElementInvalid(htmlElement); 49 | result.pass = isValid; 50 | result.message = `${ 51 | result.pass 52 | ? `${printSuccess('PASSED')} ${printSecSuccess( 53 | `Expected the element ${printSuccess(getTag(htmlElement))} not to be invalid, and it ${printSuccess( 54 | "isn't invalid" 55 | )}.` 56 | )}` 57 | : `${printError('FAILED')} ${printSecError( 58 | `Expected the element ${printError(getTag(htmlElement))} not to be invalid, and it ${printError( 59 | 'is invalid' 60 | )}.` 61 | )}` 62 | }`; 63 | return result; 64 | }, 65 | }; 66 | } 67 | 68 | export function toBeValid() { 69 | return { 70 | compare: function (htmlElement) { 71 | checkHtmlElement(htmlElement); 72 | let result = {}; 73 | const isValid = !isElementInvalid(htmlElement); 74 | result.pass = isValid; 75 | result.message = `${ 76 | result.pass 77 | ? `${printSuccess('PASSED')} ${printSecSuccess( 78 | `Expected the element ${printSuccess(getTag(htmlElement))} to be valid, and it ${printSuccess( 79 | 'is valid' 80 | )}.` 81 | )}` 82 | : `${printError('FAILED')} ${printSecError( 83 | `Expected the element ${printError(getTag(htmlElement))} to be valid, and it ${printError( 84 | "isn't valid" 85 | )}.` 86 | )}` 87 | }`; 88 | return result; 89 | }, 90 | negativeCompare: function (htmlElement) { 91 | checkHtmlElement(htmlElement); 92 | let result = {}; 93 | const isInvalid = isElementInvalid(htmlElement); 94 | result.pass = isInvalid; 95 | result.message = `${ 96 | result.pass 97 | ? `${printSuccess('PASSED')} ${printSecSuccess( 98 | `Expected the element ${printSuccess(getTag(htmlElement))} not to be valid, and it ${printSuccess( 99 | "isn't valid" 100 | )}.` 101 | )}` 102 | : `${printError('FAILED')} ${printSecError( 103 | `Expected the element ${printError(getTag(htmlElement))} not to be valid, and it ${printError( 104 | 'is valid' 105 | )}.` 106 | )}` 107 | }`; 108 | return result; 109 | }, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/toHaveValue.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getSingleElementValue, compareArraysAsSet, isEqualWith, getTag } from './utils'; 2 | import { printSecWarning, printWarning, printSecError, printError, printSecSuccess, printSuccess } from './printers'; 3 | 4 | export function toHaveValue() { 5 | return { 6 | compare: function (htmlElement, expectedValue) { 7 | checkHtmlElement(htmlElement); 8 | let result = {}; 9 | if (getTag(htmlElement) === 'input' && ['checkbox', 'radio'].includes(htmlElement.type)) { 10 | throw new Error( 11 | printSecWarning( 12 | `${printError('FAILED')} input elements with ${printWarning( 13 | 'type="checkbox/radio"' 14 | )} cannot be used with ${printWarning('.toHaveValue()')}. Use ${printSuccess( 15 | '.toBeChecked()' 16 | )} for type="checkbox" or ${printSuccess('.toHaveFormValues()')} instead.` 17 | ) 18 | ); 19 | } 20 | const receivedValue = getSingleElementValue(htmlElement); 21 | const expectsValue = expectedValue !== undefined; 22 | let expectedTypedValue = expectedValue; 23 | let receivedTypedValue = receivedValue; 24 | if (expectedValue == receivedValue && expectedValue !== receivedValue) { 25 | expectedTypedValue = `${expectedValue} (${typeof expectedValue})`; 26 | receivedTypedValue = `${receivedValue} (${typeof receivedValue})`; 27 | } 28 | result.pass = expectsValue 29 | ? isEqualWith(receivedValue, expectedValue, compareArraysAsSet) 30 | : Boolean(receivedValue); 31 | result.message = result.pass 32 | ? `${printSuccess('PASSED')} ${printSecSuccess( 33 | `Expected the provided ${printSuccess(getTag(htmlElement))} to have value ${printSuccess( 34 | `${expectsValue ? expectedTypedValue : '(any)'}` 35 | )}.\nReceived ${printSuccess(receivedTypedValue)}.` 36 | )}` 37 | : `${printError('FAILED')} ${printSecError( 38 | `Expected the provided ${printError(getTag(htmlElement))} to have value ${printError( 39 | `${expectsValue ? expectedTypedValue : '(any)'}` 40 | )}.\nReceived ${printError(receivedTypedValue)}.` 41 | )}`; 42 | return result; 43 | }, 44 | negativeCompare: function (htmlElement, expectedValue) { 45 | checkHtmlElement(htmlElement); 46 | let result = {}; 47 | if (getTag(htmlElement) === 'input' && ['checkbox', 'radio'].includes(htmlElement.type)) { 48 | throw new Error( 49 | printSecWarning( 50 | `${printError('FAILED')} input elements with ${printWarning( 51 | 'type="checkbox/radio"' 52 | )} cannot be used with ${printWarning('.toHaveValue()')}. Use ${printSuccess( 53 | '.toBeChecked()' 54 | )} for type="checkbox" or ${printSuccess('.toHaveFormValues()')} instead.` 55 | ) 56 | ); 57 | } 58 | const receivedValue = getSingleElementValue(htmlElement); 59 | const expectsValue = expectedValue !== undefined; 60 | let expectedTypedValue = expectedValue; 61 | let receivedTypedValue = receivedValue; 62 | if (expectedValue == receivedValue && expectedValue !== receivedValue) { 63 | expectedTypedValue = `${expectedValue} (${typeof expectedValue})`; 64 | receivedTypedValue = `${receivedValue} (${typeof receivedValue})`; 65 | } 66 | result.pass = expectsValue 67 | ? !isEqualWith(receivedValue, expectedValue, compareArraysAsSet) 68 | : Boolean(!receivedValue); 69 | result.message = result.pass 70 | ? `${printSuccess('PASSED')} ${printSecSuccess( 71 | `Expected the provided ${printSuccess(getTag(htmlElement))} not to have value ${printSuccess( 72 | `${expectsValue ? expectedTypedValue : '(any)'}` 73 | )}.\nReceived ${printSuccess(receivedTypedValue)}.` 74 | )}` 75 | : `${printError('FAILED')} ${printSecError( 76 | `Expected the provided ${printError(getTag(htmlElement))} not to have value ${printError( 77 | `${expectsValue ? expectedTypedValue : '(any)'}` 78 | )}.\nReceived ${printError(receivedTypedValue)}.` 79 | )}`; 80 | return result; 81 | }, 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/toBeChecked.js: -------------------------------------------------------------------------------- 1 | import { roles } from 'aria-query'; 2 | import { checkHtmlElement, getTag, toSentence } from './utils'; 3 | import { printSecWarning, printWarning, printSuccess, printSecSuccess, printError, printSecError } from './printers'; 4 | 5 | function roleSupportsChecked(role) { 6 | return roles.get(role)?.props['aria-checked'] !== undefined; 7 | } 8 | 9 | function supportedRoles() { 10 | return Array.from(roles.keys()).filter(roleSupportsChecked); 11 | } 12 | 13 | function supportedRolesSentence() { 14 | return toSentence( 15 | supportedRoles().map(role => `role="${role}"`), 16 | { lastWordConnector: ' or ' } 17 | ); 18 | } 19 | 20 | function isValidInput(htmlElement) { 21 | return getTag(htmlElement) === 'input' && ['checkbox', 'radio'].includes(htmlElement.type); 22 | } 23 | 24 | function isValidAriaElement(htmlElement) { 25 | return ( 26 | roleSupportsChecked(htmlElement.getAttribute('role')) && 27 | ['true', 'false'].includes(htmlElement.getAttribute('aria-checked')) 28 | ); 29 | } 30 | 31 | function isChecked(htmlElement) { 32 | if (isValidInput(htmlElement)) return htmlElement.checked; 33 | return htmlElement.getAttribute('aria-checked') === 'true'; 34 | } 35 | 36 | export function toBeChecked() { 37 | return { 38 | compare: function (htmlElement) { 39 | checkHtmlElement(htmlElement); 40 | let result = {}; 41 | const validInput = isValidInput(htmlElement); 42 | const validAriaElement = isValidAriaElement(htmlElement); 43 | if (!validInput && !validAriaElement) { 44 | result.pass = false; 45 | result.message = `${printError('FAILED')} ${printSecWarning( 46 | `Only inputs with type='checkbox/radio' or elements with ${supportedRolesSentence()} and a valid aria-checked attribute can be used with ${printWarning( 47 | '.toBeChecked' 48 | )}. Use ${printSuccess(`.toHaveValue()`)} instead.` 49 | )}`; 50 | return result; 51 | } 52 | const checkedInput = isChecked(htmlElement); 53 | result.pass = checkedInput; 54 | result.message = `${ 55 | result.pass 56 | ? `${printSuccess('PASSED')} ${printSecSuccess( 57 | `Expected the element ${printSuccess(getTag(htmlElement))} to be checked and it ${printSuccess( 58 | 'is checked' 59 | )}.` 60 | )}` 61 | : `${printError('FAILED')} ${printSecError( 62 | `Expected the element ${printError(getTag(htmlElement))} ${printError( 63 | `type="${htmlElement.type}"` 64 | )} to be checked and it ${printError("isn't checked")}.` 65 | )}` 66 | }`; 67 | return result; 68 | }, 69 | negativeCompare: function (htmlElement) { 70 | checkHtmlElement(htmlElement); 71 | let result = {}; 72 | const validInput = isValidInput(htmlElement); 73 | const validAriaElement = isValidAriaElement(htmlElement); 74 | if (!validInput && !validAriaElement) { 75 | result.pass = false; 76 | result.message = `${printError('FAILED')} ${printSecWarning( 77 | `Only inputs with type='checkbox/radio' or elements with role='checkbox/radio/switch' and a valid aria-checked attribute can be used with` 78 | )} ${printWarning(`.toBeChecked()`)}${printSecWarning('. Use')} ${printSuccess( 79 | `.toHaveValue()` 80 | )}${printSecWarning(' instead.')}`; 81 | return result; 82 | } 83 | const notCheckedInput = !isChecked(htmlElement); 84 | result.pass = notCheckedInput; 85 | result.message = `${ 86 | result.pass 87 | ? `${printSuccess('PASSED')} ${printSecSuccess( 88 | `Expected the element ${printSuccess(getTag(htmlElement))} not to be checked and it ${printSuccess( 89 | "isn't checked" 90 | )}.` 91 | )}` 92 | : `${printError('FAILED')} ${printSecError( 93 | `Expected the element ${printError(getTag(htmlElement))} ${printError( 94 | `type="${htmlElement.type}"` 95 | )} not to be checked and it ${printError('is checked')}.` 96 | )}` 97 | }`; 98 | return result; 99 | }, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/__tests__/toHaveTextContent.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveTextContent } from '../toHaveTextContent'; 3 | 4 | describe('.toHaveTextContent', () => { 5 | const { compare, negativeCompare } = toHaveTextContent(); 6 | 7 | it('positive test cases', () => { 8 | const { queryByTestId } = render(` 9 | 2 10 | `); 11 | const countValue = queryByTestId('count-value'); 12 | 13 | expect(countValue).toHaveTextContent('2'); 14 | expect(countValue).toHaveTextContent(2); 15 | expect(countValue).toHaveTextContent(/2/); 16 | expect(countValue).not.toHaveTextContent('21'); 17 | }); 18 | 19 | it('negative test cases', () => { 20 | const { queryByTestId } = render(` 21 | 2 22 | `); 23 | const countValue = queryByTestId('count-value'); 24 | const { message: positiveMessage, pass: positivePass } = compare(countValue, '3'); 25 | const { message: negativeMessage, pass: negativePass } = negativeCompare(countValue, '2'); 26 | 27 | expect(positivePass).toBeFalse(); 28 | expect(positiveMessage).toMatch(/Expected.*'2'.*to match.*'3'/); 29 | expect(negativePass).toBeFalse(); 30 | expect(negativeMessage).toMatch(/Expected.*not to match/); 31 | }); 32 | 33 | it('normalizes whitespaces by default', () => { 34 | const { container } = render(` 35 | 36 | Step 37 | 1 38 | of 39 | 4 40 | 41 | `); 42 | 43 | expect(container.querySelector('span')).toHaveTextContent('Step 1 of 4'); 44 | }); 45 | 46 | it('normalizing whitespace is an option and can be turned off', () => { 47 | const { container } = render(` 48 |   Step 1 of 4 49 | `); 50 | 51 | expect(container.querySelector('span')).toHaveTextContent(' Step 1 of 4', { 52 | normalizeWhitespace: false, 53 | }); 54 | 55 | expect(container.querySelector('span')).not.toHaveTextContent('Step 2 of 3', { 56 | normalizeWhitespace: false, 57 | }); 58 | }); 59 | 60 | it('can handle multiple levels', () => { 61 | const { container } = render(` 62 | Step 1 63 | 64 | of 4 65 | `); 66 | 67 | expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4'); 68 | }); 69 | 70 | it('can handle multiple levels spread across descendants', () => { 71 | const { container } = render(` 72 | 73 | Step 74 | 1 75 | of 76 | 4 77 | 78 | `); 79 | 80 | expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4'); 81 | }); 82 | 83 | it("doesn't throw for empty content", () => { 84 | const { container } = render(` 85 | 86 | `); 87 | 88 | expect(container.querySelector('span')).toHaveTextContent(''); 89 | }); 90 | 91 | it('is case-sensitive', () => { 92 | const { container } = render(` 93 | Sensitive text 94 | `); 95 | 96 | expect(container.querySelector('span')).toHaveTextContent('Sensitive text'); 97 | expect(container.querySelector('span')).not.toHaveTextContent('sensitive text'); 98 | }); 99 | 100 | it('when matching an empty string and an element with content, suggest using toBeEmptyDOMElement instead', () => { 101 | const { container } = render(` 102 | Not empty 103 | `); 104 | const { message: positiveMessage, pass: positivePass } = compare(container.querySelector('span'), ''); 105 | const { message: negativeMessage, pass: negativePass } = negativeCompare(container.querySelector('span'), ''); 106 | 107 | expect(positivePass).toBeFalse(); 108 | expect(positiveMessage).toMatch( 109 | /Checking with an empty string will always match\. Try using.*.toBeEmptyDOMElement\(\)/ 110 | ); 111 | 112 | expect(negativePass).toBeFalse(); 113 | expect(negativeMessage).toMatch( 114 | /Checking with an empty string will always match\. Try using.*.toBeEmptyDOMElement\(\)/ 115 | ); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/toHaveDisplayValue.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, matches, getTag } from './utils'; 2 | import { printSuccess, printSecSuccess, printSecError, printError, printSecWarning, printWarning } from './printers'; 3 | 4 | function getValues(tagName, htmlElement) { 5 | return tagName === 'select' 6 | ? Array.from(htmlElement) 7 | .filter(option => option.selected) 8 | .map(option => option.textContent) 9 | : [htmlElement.value]; 10 | } 11 | 12 | function getExpectedValues(expectedValue) { 13 | return expectedValue instanceof Array ? expectedValue : [expectedValue]; 14 | } 15 | 16 | function getNumberOfMatchesBetweenArrays(arrayBase, array) { 17 | return array.filter(expected => arrayBase.filter(value => matches(value, expected)).length).length; 18 | } 19 | 20 | export function toHaveDisplayValue() { 21 | return { 22 | compare: function (htmlElement, expectedValue) { 23 | checkHtmlElement(htmlElement); 24 | let result = {}; 25 | const tagName = getTag(htmlElement); 26 | 27 | if (!['select', 'input', 'textarea'].includes(tagName)) { 28 | throw new Error( 29 | printSecWarning( 30 | `${printError('FAILED')} .toHaveDisplayValue() supports only ${printWarning('input')}, ${printWarning( 31 | 'textarea' 32 | )} or ${printWarning('select')} elements. Try using another matcher instead.` 33 | ) 34 | ); 35 | } 36 | 37 | if (tagName === 'input' && ['radio', 'checkbox'].includes(htmlElement.type)) { 38 | throw new Error( 39 | printSecWarning( 40 | `${printError('FAILED')} .toHaveDisplayValue() currently does not support ${printWarning( 41 | `input[type="${htmlElement.type}"]` 42 | )}, try with another matcher instead.` 43 | ) 44 | ); 45 | } 46 | 47 | const values = getValues(tagName, htmlElement); 48 | const expectedValues = getExpectedValues(expectedValue); 49 | const numberOfMatchesWithValues = getNumberOfMatchesBetweenArrays(values, expectedValues); 50 | const matchedWithAllValues = numberOfMatchesWithValues === values.length; 51 | const matchedWithAllExpectedValues = numberOfMatchesWithValues === expectedValues.length; 52 | 53 | result.pass = matchedWithAllValues && matchedWithAllExpectedValues; 54 | result.message = result.pass 55 | ? `${printSuccess('PASSED')} ${printSecSuccess( 56 | `Expected the element ${printSuccess(getTag(htmlElement))} to have display value ${printSuccess( 57 | `'${expectedValue}'` 58 | )}. Received ${printSuccess(`'${values}'`)}` 59 | )}` 60 | : `${printError('FAILED')} ${printSecError( 61 | `Expected the element ${printError(getTag(htmlElement))} to have display value ${printError( 62 | `'${expectedValue}'` 63 | )}. Received ${printError(`'${values}'`)}` 64 | )}`; 65 | return result; 66 | }, 67 | negativeCompare: function (htmlElement, expectedValue) { 68 | checkHtmlElement(htmlElement); 69 | let result = {}; 70 | const tagName = getTag(htmlElement); 71 | 72 | if (!['select', 'input', 'textarea'].includes(tagName)) { 73 | throw new Error( 74 | printSecWarning( 75 | `${printError('FAILED')} .toHaveDisplayValue() supports only ${printWarning('input')}, ${printWarning( 76 | 'textarea' 77 | )} or ${printWarning('select')} elements. Try using another matcher instead.` 78 | ) 79 | ); 80 | } 81 | 82 | const values = getValues(tagName, htmlElement); 83 | const expectedValues = getExpectedValues(expectedValue); 84 | const numberOfMatchesWithValues = getNumberOfMatchesBetweenArrays(values, expectedValues); 85 | const matchedWithAllValues = numberOfMatchesWithValues === values.length; 86 | const matchedWithAllExpectedValues = numberOfMatchesWithValues === expectedValues.length; 87 | 88 | result.pass = !(matchedWithAllValues && matchedWithAllExpectedValues); 89 | result.message = result.pass 90 | ? `${printSuccess('PASSED')} ${printSecSuccess( 91 | `Expected the element ${printSuccess(getTag(htmlElement))} not to have display value ${printSuccess( 92 | `'${expectedValue}'` 93 | )}. Received ${printSuccess(`'${values}'`)}` 94 | )}` 95 | : `${printError('FAILED')} ${printSecError( 96 | `Expected the element ${printError(getTag(htmlElement))} not to have display value ${printError( 97 | `'${expectedValue}'` 98 | )}. Received ${printError(`'${values}'`)}` 99 | )}`; 100 | return result; 101 | }, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/__tests__/toHaveAccessibleDescription.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveAccessibleDescription } from '../toHaveAccessibleDescription'; 3 | 4 | describe('.toHaveAccessibleDescription', () => { 5 | it('works with the link title attribute', () => { 6 | const fakeMatchersUtils = { 7 | equals: Object.is, 8 | }; 9 | 10 | const { queryByTestId } = render(` 11 |
12 | Start 13 | About 14 |
15 | `); 16 | 17 | const link = queryByTestId('link'); 18 | expect(link).toHaveAccessibleDescription(); 19 | expect(link).toHaveAccessibleDescription('A link to start over'); 20 | expect(link).not.toHaveAccessibleDescription('Home page'); 21 | 22 | const { compare } = toHaveAccessibleDescription(fakeMatchersUtils); 23 | { 24 | const { pass, message } = compare(link, 'Invalid description'); 25 | expect(pass).toBeFalse(); 26 | expect(message).toMatch(/expected element to have accessible description/i); 27 | } 28 | 29 | const extraLink = queryByTestId('extra-link'); 30 | expect(extraLink).not.toHaveAccessibleDescription(); 31 | { 32 | const { pass, message } = compare(link, 'Invalid description'); 33 | expect(pass).toBeFalse(); 34 | expect(message).toMatch(/expected element to have accessible description/i); 35 | } 36 | }); 37 | 38 | it('works with aria-describedby attributes', () => { 39 | const fakeMatchersUtils = { 40 | equals: Object.is, 41 | }; 42 | 43 | const { queryByTestId } = render(` 44 |
45 | User profile pic 46 | Company logo 47 | The logo of Our Company 48 |
49 | `); 50 | 51 | const avatar = queryByTestId('avatar'); 52 | expect(avatar).not.toHaveAccessibleDescription(); 53 | const { compare } = toHaveAccessibleDescription(fakeMatchersUtils); 54 | { 55 | const { pass, message } = compare(avatar, 'User profile pic'); 56 | expect(pass).toBeFalse(); 57 | expect(message).toMatch(/expected element to have accessible description/i); 58 | } 59 | 60 | const logo = queryByTestId('logo'); 61 | expect(logo).not.toHaveAccessibleDescription('Company logo'); 62 | expect(logo).toHaveAccessibleDescription('The logo of Our Company'); 63 | expect(logo).toHaveAccessibleDescription(/logo of our company/i); 64 | expect(logo).toHaveAccessibleDescription(jasmine.stringContaining('logo of Our Company')); 65 | { 66 | const { pass, message } = compare(logo, "Our company's logo"); 67 | expect(pass).toBeFalse(); 68 | expect(message).toMatch(/expected element to have accessible description/i); 69 | } 70 | }); 71 | 72 | it('handles multiple ids', () => { 73 | const { queryByTestId } = render(` 74 |
75 |
First description
76 |
Second description
77 |
Third description
78 | 79 |
80 |
81 | `); 82 | 83 | expect(queryByTestId('multiple')).toHaveAccessibleDescription( 84 | 'First description Second description Third description' 85 | ); 86 | expect(queryByTestId('multiple')).toHaveAccessibleDescription(/Second description Third/); 87 | expect(queryByTestId('multiple')).toHaveAccessibleDescription(jasmine.stringContaining('Second description Third')); 88 | expect(queryByTestId('multiple')).toHaveAccessibleDescription(jasmine.stringMatching(/Second description Third/)); 89 | expect(queryByTestId('multiple')).not.toHaveAccessibleDescription('Something else'); 90 | expect(queryByTestId('multiple')).not.toHaveAccessibleDescription('First'); 91 | }); 92 | 93 | it('normalizes whitespace', () => { 94 | const { queryByTestId } = render(` 95 |
96 | Step 97 | 1 98 | of 99 | 4 100 |
101 |
102 | And 103 | extra 104 | description 105 |
106 |
107 | `); 108 | 109 | expect(queryByTestId('target')).toHaveAccessibleDescription('Step 1 of 4 And extra description'); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/toBeDisabled.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printError, printSecError, printSuccess, printSecSuccess } from './printers'; 3 | 4 | const DISABLED_FORM_TAGS = ['fieldset', 'input', 'select', 'optgroup', 'option', 'button', 'textarea']; 5 | 6 | function isFirstLegendChildOfFieldset(htmlElement, parentElement) { 7 | return ( 8 | getTag(htmlElement) === 'legend' && 9 | getTag(parentElement) === 'fieldset' && 10 | htmlElement.isSameNode(Array.from(parentElement.children).find(child => getTag(child) === 'legend')) 11 | ); 12 | } 13 | 14 | function canElementBeDisabled(htmlElement) { 15 | return DISABLED_FORM_TAGS.includes(getTag(htmlElement)); 16 | } 17 | 18 | function isElementDisabled(htmlElement) { 19 | return canElementBeDisabled(htmlElement) && htmlElement.hasAttribute('disabled'); 20 | } 21 | 22 | function isElementDisabledByParent(htmlElement, parentElement) { 23 | return isElementDisabled(parentElement) && !isFirstLegendChildOfFieldset(htmlElement, parentElement); 24 | } 25 | 26 | function isAncestorDisabled(htmlElement) { 27 | const parent = htmlElement.parentElement; 28 | return Boolean(parent) && (isElementDisabledByParent(htmlElement, parent) || isAncestorDisabled(parent)); 29 | } 30 | 31 | function isElementOrAncestorDisabled(htmlElement) { 32 | return canElementBeDisabled(htmlElement) && (isElementDisabled(htmlElement) || isAncestorDisabled(htmlElement)); 33 | } 34 | 35 | export function toBeDisabled() { 36 | return { 37 | compare: function (htmlElement) { 38 | checkHtmlElement(htmlElement); 39 | let result = {}; 40 | const isDisabled = isElementOrAncestorDisabled(htmlElement); 41 | result.pass = isDisabled; 42 | result.message = `${ 43 | result.pass 44 | ? `${printSuccess('PASSED')} ${printSecSuccess( 45 | `Expected the element ${printSuccess(getTag(htmlElement))} to be disabled and it ${printSuccess( 46 | 'is disabled' 47 | )}.` 48 | )}` 49 | : `${printError('FAILED')} ${printSecError( 50 | `Expected the element ${printError(getTag(htmlElement))} to be disabled and it ${printError( 51 | "isn't disabled" 52 | )}.` 53 | )}` 54 | }`; 55 | return result; 56 | }, 57 | negativeCompare: function (htmlElement) { 58 | checkHtmlElement(htmlElement); 59 | let result = {}; 60 | const isNotDisabled = !isElementOrAncestorDisabled(htmlElement); 61 | result.pass = isNotDisabled; 62 | result.message = `${ 63 | result.pass 64 | ? `${printSuccess('PASSED')} ${printSecSuccess( 65 | `Expected the element ${printSuccess(getTag(htmlElement))} not to be disabled and it ${printSuccess( 66 | "isn't disabled" 67 | )}.` 68 | )}` 69 | : `${printError('FAILED')} ${printSecError( 70 | `Expected the element ${printError(getTag(htmlElement))} not to be disabled and it ${printError( 71 | 'is disabled' 72 | )}.` 73 | )}` 74 | }`; 75 | return result; 76 | }, 77 | }; 78 | } 79 | 80 | export function toBeEnabled() { 81 | return { 82 | compare: function (htmlElement) { 83 | checkHtmlElement(htmlElement); 84 | let result = {}; 85 | const isEnabled = !isElementOrAncestorDisabled(htmlElement); 86 | result.pass = isEnabled; 87 | result.message = `${ 88 | result.pass 89 | ? `${printSuccess('PASSED')} ${printSecSuccess( 90 | `Expected the element ${printSuccess(getTag(htmlElement))} to be enabled and it ${printSuccess( 91 | 'is enabled' 92 | )}.` 93 | )}` 94 | : `${printError('FAILED')} ${printSecError( 95 | `Expected the element ${printError(getTag(htmlElement))} to be enabled and it ${printError( 96 | "isn't enabled" 97 | )}.` 98 | )}` 99 | }`; 100 | return result; 101 | }, 102 | negativeCompare: function (htmlElement) { 103 | checkHtmlElement(htmlElement); 104 | let result = {}; 105 | const isEnabled = !isElementOrAncestorDisabled(htmlElement); 106 | result.pass = !isEnabled; 107 | result.message = `${ 108 | result.pass 109 | ? `${printSuccess('PASSED')} ${printSecSuccess( 110 | `Expected the element ${printSuccess(getTag(htmlElement))} not to be enabled and it ${printSuccess( 111 | "isn't enabled" 112 | )}.` 113 | )}` 114 | : `${printError('FAILED')} ${printSecError( 115 | `Expected the element ${printError(getTag(htmlElement))} not to be enabled and it ${printError( 116 | 'is enabled' 117 | )}.` 118 | )}` 119 | }`; 120 | return result; 121 | }, 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /src/__tests__/toBeDisabled.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeDisabled } from '../toBeDisabled'; 3 | 4 | describe('.toBeDisabled()', () => { 5 | const { compare, negativeCompare } = toBeDisabled(); 6 | const { queryByTestId } = render(` 7 |
8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 25 |
26 | Nested anchor 27 |
28 | Anchor 29 |
30 | `); 31 | 32 | it('positive compare', () => { 33 | const { message, pass } = compare(queryByTestId('anchor')); 34 | expect(pass).toBeFalse(); 35 | expect(message).toMatch(/Expected the element.*to be disabled and it.*isn't disabled.*\./); 36 | 37 | expect(queryByTestId('button')).toBeDisabled(); 38 | expect(queryByTestId('textarea')).toBeDisabled(); 39 | expect(queryByTestId('input')).toBeDisabled(); 40 | 41 | expect(queryByTestId('fieldset')).toBeDisabled(); 42 | expect(queryByTestId('fieldset-child')).toBeDisabled(); 43 | 44 | expect(queryByTestId('nested-button')).toBeDisabled(); 45 | expect(queryByTestId('nested-select')).toBeDisabled(); 46 | expect(queryByTestId('nested-optgroup')).toBeDisabled(); 47 | expect(queryByTestId('nested-option')).toBeDisabled(); 48 | }); 49 | 50 | it('negative compare', () => { 51 | const { message, pass } = negativeCompare(queryByTestId('button')); 52 | expect(pass).toBeFalse(); 53 | expect(message).toMatch(/Expected the element.*not to be disabled and it.*is disabled.*\./); 54 | 55 | expect(queryByTestId('div')).not.toBeDisabled(); 56 | expect(queryByTestId('div-child')).not.toBeDisabled(); 57 | 58 | expect(queryByTestId('nested-anchor')).not.toBeDisabled(); 59 | expect(queryByTestId('anchor')).not.toBeDisabled(); 60 | }); 61 | }); 62 | 63 | describe('.toBeDisabled() w/ fieldset>legend', () => { 64 | const { queryByTestId } = render(` 65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 |
74 |
75 | 76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |
91 |
92 | 93 | 94 | 95 |
96 |
97 |
98 | `); 99 | 100 | it('positive compare', () => { 101 | expect(queryByTestId('inherited-element')).toBeDisabled(); 102 | expect(queryByTestId('second-legend-element')).toBeDisabled(); 103 | expect(queryByTestId('outer-fieldset-element')).toBeDisabled(); 104 | }); 105 | 106 | it('negative compare', () => { 107 | expect(queryByTestId('nested-inside-legend-element')).not.toBeDisabled(); 108 | expect(queryByTestId('inside-legend-element')).not.toBeDisabled(); 109 | expect(queryByTestId('first-legend-element')).not.toBeDisabled(); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/toHaveStyle.js: -------------------------------------------------------------------------------- 1 | import cssParse from 'css/lib/parse'; 2 | import { checkHtmlElement, getTag, InvalidCSSError } from './utils'; 3 | import { printSecSuccess, printSuccess, printSecError, printError } from './printers'; 4 | 5 | function parseCSS(css, ...args) { 6 | const ast = cssParse(`selector { ${css} }`, { 7 | silent: true, 8 | }).stylesheet; 9 | if (ast.parsingErrors && ast.parsingErrors.length > 0) { 10 | const { reason, line } = ast.parsingErrors[0]; 11 | throw new InvalidCSSError( 12 | { 13 | css, 14 | message: printSecError(`Syntax error parsing expected styles: ${reason} on ${printError(`line ${line}`)}`), 15 | }, 16 | ...args 17 | ); 18 | } 19 | const parsedRules = ast.rules[0].declarations 20 | .filter(declaration => declaration.type === 'declaration') 21 | .reduce((obj, { property, value }) => Object.assign(obj, { [property]: value }), {}); 22 | return parsedRules; 23 | } 24 | 25 | function parseJStoCSS(document, styles) { 26 | const sandboxElement = document.createElement('div'); 27 | Object.assign(sandboxElement.style, styles); 28 | return sandboxElement.style.cssText; 29 | } 30 | 31 | function getStyleDeclaration(document, css) { 32 | const styles = {}; 33 | 34 | // The next block is necessary to normalize colors 35 | const copy = document.createElement('div'); 36 | Object.keys(css).forEach(prop => { 37 | copy.style[prop] = css[prop]; 38 | styles[prop] = copy.style[prop]; 39 | }); 40 | 41 | return styles; 42 | } 43 | 44 | function styleIsSubset(styles, computedStyle) { 45 | return ( 46 | !!Object.keys(styles).length && 47 | Object.entries(styles).every( 48 | ([prop, value]) => computedStyle[prop] === value || computedStyle.getPropertyValue(prop.toLowerCase()) === value 49 | ) 50 | ); 51 | } 52 | 53 | function getCSStoParse(document, styles) { 54 | return typeof styles === 'object' ? parseJStoCSS(document, styles) : styles; 55 | } 56 | 57 | function printoutStyles(styles) { 58 | return Object.keys(styles) 59 | .sort() 60 | .map(prop => `${prop}: ${styles[prop]};`) 61 | .join('\n'); 62 | } 63 | 64 | function expectedStyleDiff(expected, computedStyles) { 65 | const received = Array.from(computedStyles) 66 | .filter(prop => expected[prop] !== undefined) 67 | .reduce( 68 | (obj, prop) => 69 | Object.assign(obj, { 70 | [prop]: computedStyles.getPropertyValue(prop), 71 | }), 72 | {} 73 | ); 74 | const receivedOutput = printoutStyles(received); 75 | return receivedOutput; 76 | } 77 | 78 | export function toHaveStyle() { 79 | return { 80 | compare: function (htmlElement, styles) { 81 | checkHtmlElement(htmlElement); 82 | let result = {}; 83 | const cssToParse = getCSStoParse(htmlElement.ownerDocument, styles); 84 | const parsedCSS = parseCSS(cssToParse); 85 | const { getComputedStyle } = htmlElement.ownerDocument.defaultView; 86 | const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS); 87 | const received = getComputedStyle(htmlElement); 88 | result.pass = styleIsSubset(expected, received); 89 | result.message = result.pass 90 | ? `${printSuccess('PASSED')} ${printSecSuccess( 91 | `Expected the provided ${printSuccess(getTag(htmlElement))} element to have styles:\n${printSuccess( 92 | styles 93 | )}\nReceived:\n\n${printSuccess(expectedStyleDiff(expected, received))}` 94 | )}` 95 | : `${printError('FAILED')} ${printSecError( 96 | `Expected the provided ${printError(getTag(htmlElement))} element to have styles:\n${printError( 97 | styles 98 | )}\nReceived:\n\n${printError(expectedStyleDiff(expected, received))}` 99 | )}`; 100 | return result; 101 | }, 102 | negativeCompare: function (htmlElement, styles) { 103 | checkHtmlElement(htmlElement); 104 | let result = {}; 105 | const cssToParse = getCSStoParse(htmlElement.ownerDocument, styles); 106 | const parsedCSS = parseCSS(cssToParse); 107 | const { getComputedStyle } = htmlElement.ownerDocument.defaultView; 108 | const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS); 109 | const received = getComputedStyle(htmlElement); 110 | result.pass = !styleIsSubset(expected, received); 111 | result.message = result.pass 112 | ? `${printSuccess('PASSED')} ${printSecSuccess( 113 | `Expected the provided ${printSuccess(getTag(htmlElement))} element not to have styles:\n${printSuccess( 114 | styles 115 | )}\nReceived:\n\n${printSuccess(expectedStyleDiff(expected, received))}` 116 | )}` 117 | : `${printError('FAILED')} ${printSecError( 118 | `Expected the provided ${printError(getTag(htmlElement))} element not to have styles:\n${printError( 119 | styles 120 | )}\nReceived:\n\n${printError(expectedStyleDiff(expected, received))}` 121 | )}`; 122 | return result; 123 | }, 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/__tests__/toBeRequired.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeRequired } from '../toBeRequired'; 3 | 4 | describe('.toBeRequired', () => { 5 | const { compare, negativeCompare } = toBeRequired(); 6 | 7 | it('positive test cases', () => { 8 | const { queryByTestId } = render(` 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | `); 22 | 23 | expect(queryByTestId('required-input')).toBeRequired(); 24 | expect(queryByTestId('aria-required-input')).toBeRequired(); 25 | expect(queryByTestId('conflicted-input')).toBeRequired(); 26 | expect(queryByTestId('not-required-input')).not.toBeRequired(); 27 | expect(queryByTestId('basic-input')).not.toBeRequired(); 28 | expect(queryByTestId('unsupported-type')).not.toBeRequired(); 29 | expect(queryByTestId('select')).toBeRequired(); 30 | expect(queryByTestId('textarea')).toBeRequired(); 31 | expect(queryByTestId('supported-role')).not.toBeRequired(); 32 | expect(queryByTestId('supported-role-aria')).toBeRequired(); 33 | }); 34 | 35 | it('negative test cases', () => { 36 | const { queryByTestId } = render(` 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | `); 50 | 51 | const { message: requiredMessage, pass: requiredPass } = negativeCompare(queryByTestId('required-input')); 52 | const { message: ariaRequiredMessage, pass: ariaRequiredPass } = negativeCompare( 53 | queryByTestId('aria-required-input') 54 | ); 55 | const { message: conflictedMessage, pass: conflictedPass } = negativeCompare(queryByTestId('conflicted-input')); 56 | const { message: notRequiredMessage, pass: notRequiredPass } = compare(queryByTestId('not-required-input')); 57 | const { message: basicInputMessage, pass: basicInputPass } = compare(queryByTestId('basic-input')); 58 | const { message: unsupportedMessage, pass: unsupportedPass } = compare(queryByTestId('unsupported-type')); 59 | const { message: selectMessage, pass: selectPass } = negativeCompare(queryByTestId('select')); 60 | const { message: textareaMessage, pass: textareaPass } = negativeCompare(queryByTestId('textarea')); 61 | const { message: supportedRoleMessage, pass: supportedRolePass } = compare(queryByTestId('supported-role')); 62 | const { message: ariaSupportedRoleMessage, pass: ariaSupportedRolePass } = negativeCompare( 63 | queryByTestId('supported-role-aria') 64 | ); 65 | 66 | expect(requiredPass).toBeFalse(); 67 | expect(requiredMessage).toMatch(/Expected.*not to be required.*is required/); 68 | 69 | expect(ariaRequiredPass).toBeFalse(); 70 | expect(ariaRequiredMessage).toMatch(/Expected.*not to be required.*is required/); 71 | 72 | expect(conflictedPass).toBeFalse(); 73 | expect(conflictedMessage).toMatch(/Expected.*not to be required.*is required/); 74 | 75 | expect(notRequiredPass).toBeFalse(); 76 | expect(notRequiredMessage).toMatch(/Expected.*to be required.*isn't required/); 77 | 78 | expect(basicInputPass).toBeFalse(); 79 | expect(basicInputMessage).toMatch(/Expected.*to be required.*isn't required/); 80 | 81 | expect(unsupportedPass).toBeFalse(); 82 | expect(unsupportedMessage).toMatch(/Expected.*to be required.*isn't required/); 83 | 84 | expect(selectPass).toBeFalse(); 85 | expect(selectMessage).toMatch(/Expected.*not to be required.*is required/); 86 | 87 | expect(textareaPass).toBeFalse(); 88 | expect(textareaMessage).toMatch(/Expected.*not to be required.*is required/); 89 | 90 | expect(supportedRolePass).toBeFalse(); 91 | expect(supportedRoleMessage).toMatch(/Expected.*to be required.*isn't required/); 92 | 93 | expect(ariaSupportedRolePass).toBeFalse(); 94 | expect(ariaSupportedRoleMessage).toMatch(/Expected.*not to be required.*is required/); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/toHaveFormValues.js: -------------------------------------------------------------------------------- 1 | import { 2 | checkHtmlElement, 3 | getSingleElementValue, 4 | compareArraysAsSet, 5 | isEqualWith, 6 | cssEscape, 7 | getTag, 8 | uniq, 9 | } from './utils'; 10 | import { printSecWarning, printWarning, printSuccess, printSecSuccess, printSecError, printError } from './printers'; 11 | 12 | function getMultiElementValue(htmlElements) { 13 | const types = uniq(htmlElements.map(htmlElement => htmlElement.type)); 14 | 15 | if (types.length !== 1) { 16 | throw new Error( 17 | printWarning(`${printError('FAILED')} Multiple form elements with the same name must be of the same type`) 18 | ); 19 | } 20 | 21 | switch (types[0]) { 22 | case 'radio': { 23 | const theChosenOne = htmlElements.find(radio => radio.checked); 24 | return theChosenOne ? theChosenOne.value : undefined; 25 | } 26 | 27 | case 'checkbox': 28 | return htmlElements.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value); 29 | 30 | default: 31 | return htmlElements.map(htmlElement => htmlElement.value); 32 | } 33 | } 34 | 35 | function getFormValue(container, name) { 36 | const htmlElements = [...container.querySelectorAll(`[name="${cssEscape(name)}"]`)]; 37 | 38 | if (htmlElements.length === 0) { 39 | return undefined; 40 | } 41 | 42 | switch (htmlElements.length) { 43 | case 1: 44 | return getSingleElementValue(htmlElements[0]); 45 | 46 | default: 47 | return getMultiElementValue(htmlElements); 48 | } 49 | } 50 | 51 | function getPureName(name) { 52 | return /\[\]$/.test(name) ? name.slice(0, -2) : name; 53 | } 54 | 55 | function getAllFormValues(container) { 56 | const names = Array.from(container.elements).map(htmlElement => htmlElement.name); 57 | return names.reduce( 58 | (obj, name) => ({ 59 | ...obj, 60 | [getPureName(name)]: getFormValue(container, name), 61 | }), 62 | {} 63 | ); 64 | } 65 | 66 | export function toHaveFormValues() { 67 | return { 68 | compare: function (formElement, expectedValues) { 69 | checkHtmlElement(formElement); 70 | if (!formElement.elements) { 71 | throw new Error( 72 | `${printError('FAILED')} ${printSecWarning( 73 | `.toHaveFormValues() must be called on a ${printWarning('form')} or a ${printWarning('fieldset')} element.` 74 | )}` 75 | ); 76 | } 77 | let result = {}; 78 | const formValues = getAllFormValues(formElement); 79 | const commonKeyValues = Object.keys(formValues) 80 | .filter(key => expectedValues.hasOwnProperty(key)) 81 | .reduce((obj, key) => ({ ...obj, [key]: formValues[key] }), {}); 82 | result.pass = Object.entries(expectedValues).every(([name, expectedValue]) => 83 | isEqualWith(formValues[name], expectedValue, compareArraysAsSet) 84 | ); 85 | result.message = result.pass 86 | ? `💯 ${printSecSuccess( 87 | `Expected the ${printSuccess(getTag(formElement))} to have values: ${printSuccess( 88 | Object.keys(expectedValues).map(key => `\n${key}: ${expectedValues[key]}`) 89 | )}.\nValues received for the expected keys: ${printSuccess( 90 | Object.keys(commonKeyValues).map(key => `\n${key}: ${commonKeyValues[key]}`) 91 | )}` 92 | )}` 93 | : `${printError('FAILED')} ${printSecError( 94 | `Expected the ${printError(getTag(formElement))} to have values: ${printError( 95 | Object.keys(expectedValues).map(key => `\n${key}: ${expectedValues[key]}`) 96 | )}.\nValues received for the expected keys: ${printError( 97 | Object.keys(commonKeyValues).map(key => `\n${key}: ${commonKeyValues[key]}`) 98 | )}` 99 | )}`; 100 | return result; 101 | }, 102 | negativeCompare: function (formElement, expectedValues) { 103 | checkHtmlElement(formElement); 104 | if (!formElement.elements) { 105 | throw new Error( 106 | `${printError('FAILED')} ${printSecWarning( 107 | `.toHaveFormValues() must be called on a ${printWarning('form')} or a ${printWarning('fieldset')} element.` 108 | )}` 109 | ); 110 | } 111 | let result = {}; 112 | const formValues = getAllFormValues(formElement); 113 | const commonKeyValues = Object.keys(formValues) 114 | .filter(key => expectedValues.hasOwnProperty(key)) 115 | .reduce((obj, key) => ({ ...obj, [key]: formValues[key] }), {}); 116 | result.pass = Object.entries(expectedValues).every( 117 | ([name, expectedValue]) => !isEqualWith(formValues[name], expectedValue, compareArraysAsSet) 118 | ); 119 | result.message = result.pass 120 | ? `💯 ${printSecSuccess( 121 | `Expected the ${printSuccess(getTag(formElement))} not to have values: ${printSuccess( 122 | Object.keys(expectedValues).map(key => `\n${key}: ${expectedValues[key]}`) 123 | )}.\nValues received for the expected keys: ${printSuccess( 124 | Object.keys(commonKeyValues).map(key => `\n${key}: ${commonKeyValues[key]}`) 125 | )}` 126 | )}` 127 | : `${printError('FAILED')} ${printSecError( 128 | `Expected the ${printError(getTag(formElement))} not to have values: ${printError( 129 | Object.keys(expectedValues).map(key => `\n${key}: ${expectedValues[key]}`) 130 | )}.\nValues received for the expected keys: ${printError( 131 | Object.keys(commonKeyValues).map(key => `\n${key}: ${commonKeyValues[key]}`) 132 | )}` 133 | )}`; 134 | return result; 135 | }, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /src/__tests__/toBeValid.test.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import { render } from './helpers/renderer'; 3 | import { toBeValid } from '../toBeInvalid'; 4 | 5 | function getDOMElement(htmlString, selector) { 6 | return new JSDOM(htmlString).window.document.querySelector(selector); 7 | } 8 | 9 | describe('.toBeValid', () => { 10 | const { compare, negativeCompare } = toBeValid(); 11 | 12 | const invalidInputHtml = ``; 13 | const invalidInputNode = getDOMElement(invalidInputHtml, 'input'); 14 | 15 | const invalidFormHtml = `
${invalidInputHtml}
`; 16 | const invalidFormNode = getDOMElement(invalidFormHtml, 'form'); 17 | 18 | it('input', () => { 19 | const { queryByTestId } = render(` 20 |
21 | 22 | 23 | 24 | 25 |
26 | `); 27 | const noAriaInvalid = queryByTestId('no-aria-invalid'); 28 | const ariaInvalid = queryByTestId('aria-invalid'); 29 | const ariaInvalidValue = queryByTestId('aria-invalid-value'); 30 | const falseAriaInvalid = queryByTestId('aria-invalid-false'); 31 | 32 | expect(noAriaInvalid).toBeValid(); 33 | expect(ariaInvalid).not.toBeValid(); 34 | expect(ariaInvalidValue).not.toBeValid(); 35 | expect(falseAriaInvalid).toBeValid(); 36 | expect(invalidInputNode).not.toBeValid(); 37 | 38 | const { message: negativeNoAriaMessage, pass: negativeNoAriaPass } = negativeCompare(noAriaInvalid); 39 | const { message: positiveAriaInvalidMessage, pass: positiveAriaInvalidPass } = compare(ariaInvalid); 40 | const { message: positiveAriaValueMessage, pass: positiveAriaValuePass } = compare(ariaInvalidValue); 41 | const { message: negativeFalseAriaMessage, pass: negativeFalseAriaPass } = negativeCompare(falseAriaInvalid); 42 | 43 | expect(negativeNoAriaPass).toBeFalse(); 44 | expect(negativeNoAriaMessage).toMatch(/Expected the element.*not to be valid.*is valid/); 45 | 46 | expect(positiveAriaInvalidPass).toBeFalse(); 47 | expect(positiveAriaInvalidMessage).toMatch(/Expected the element.*to be valid.*isn't valid/); 48 | 49 | expect(positiveAriaValuePass).toBeFalse(); 50 | expect(positiveAriaValueMessage).toMatch(/Expected the element.*to be valid.*isn't valid/); 51 | 52 | expect(negativeFalseAriaPass).toBeFalse(); 53 | expect(negativeFalseAriaMessage).toMatch(/Expected the element.*not to be valid.*is valid/); 54 | }); 55 | 56 | it('form', () => { 57 | const { queryByTestId } = render(` 58 |
59 | 60 |
61 | `); 62 | 63 | expect(queryByTestId('valid')).toBeValid(); 64 | expect(invalidFormNode).not.toBeValid(); 65 | 66 | const { message: negativeValidMessage, pass: negativeValidPass } = negativeCompare(queryByTestId('valid')); 67 | const { message: positiveInvalidMessage, pass: positiveInvalidPass } = compare(invalidFormNode); 68 | 69 | expect(negativeValidPass).toBeFalse(); 70 | expect(negativeValidMessage).toMatch(/Expected.*not to be valid.*is valid.*\./); 71 | 72 | expect(positiveInvalidPass).toBeFalse(); 73 | expect(positiveInvalidMessage).toMatch(/Expected.*to be valid.*isn't valid.*\./); 74 | }); 75 | 76 | it('other elements', () => { 77 | const { queryByTestId } = render(` 78 |
    79 |
  1. 80 |
  2. 81 |
  3. 82 |
  4. 83 |
84 | `); 85 | const valid = queryByTestId('valid'); 86 | const noAriaInvalid = queryByTestId('no-aria-invalid'); 87 | const ariaInvalid = queryByTestId('aria-invalid'); 88 | const ariaInvalidValue = queryByTestId('aria-invalid-value'); 89 | const ariaInvalidFalse = queryByTestId('aria-invalid-false'); 90 | 91 | expect(valid).toBeValid(); 92 | expect(noAriaInvalid).toBeValid(); 93 | expect(ariaInvalid).not.toBeValid(); 94 | expect(ariaInvalidValue).not.toBeValid(); 95 | expect(ariaInvalidFalse).toBeValid(); 96 | 97 | const { message: negativeValidMessage, pass: negativeValidPass } = negativeCompare(valid); 98 | const { message: negativeNoAriaMessage, pass: negativeNoAriaPass } = negativeCompare(noAriaInvalid); 99 | const { message: positiveAriaInvalidMessage, pass: positiveAriaInvalidPass } = compare(ariaInvalid); 100 | const { message: positiveAriaValueMessage, pass: positiveAriaValuePass } = compare(ariaInvalidValue); 101 | const { message: negativeFalseAriaMessage, pass: negativeFalseAriaPass } = negativeCompare(ariaInvalidFalse); 102 | 103 | expect(negativeValidPass).toBeFalse(); 104 | expect(negativeValidMessage).toMatch(/Expected.*not to be valid.*is valid.*\./); 105 | 106 | expect(negativeNoAriaPass).toBeFalse(); 107 | expect(negativeNoAriaMessage).toMatch(/Expected.*not to be valid.*is valid.*\./); 108 | 109 | expect(positiveAriaInvalidPass).toBeFalse(); 110 | expect(positiveAriaInvalidMessage).toMatch(/Expected.*to be valid.*isn't valid.*\./); 111 | 112 | expect(positiveAriaValuePass).toBeFalse(); 113 | expect(positiveAriaValueMessage).toMatch(/Expected.*to be valid.*isn't valid.*\./); 114 | 115 | expect(negativeFalseAriaPass).toBeFalse(); 116 | expect(negativeFalseAriaMessage).toMatch(/Expected.*not to be valid.*is valid.*\./); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/__tests__/toHaveDescription.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveDescription } from '../toHaveDescription'; 3 | 4 | describe('.toHaveDescription', () => { 5 | const matchersUtilMock = { 6 | equals: Object.is, 7 | }; 8 | 9 | const { compare, negativeCompare } = toHaveDescription(matchersUtilMock); 10 | 11 | it('positive test cases', () => { 12 | const { queryByTestId } = render(` 13 |
14 | Single description cases 15 |
The description
16 |
17 |
18 |
19 |
20 | `); 21 | const singleDescription = queryByTestId('single'); 22 | const invalidDescriptionId = queryByTestId('invalid-id'); 23 | const withoutDescription = queryByTestId('without'); 24 | 25 | expect(singleDescription).toHaveDescription('The description'); 26 | expect(singleDescription).toHaveDescription(/The/); 27 | expect(singleDescription).toHaveDescription(/the/i); 28 | expect(singleDescription).not.toHaveDescription(/whatever/); 29 | expect(singleDescription).not.toHaveDescription('The'); 30 | 31 | expect(invalidDescriptionId).toHaveDescription(''); 32 | expect(invalidDescriptionId).not.toHaveDescription(); 33 | 34 | expect(withoutDescription).toHaveDescription(''); 35 | expect(withoutDescription).not.toHaveDescription(); 36 | }); 37 | 38 | it('cases w/ multiple description ids', () => { 39 | const { queryByTestId } = render(` 40 |
First description
41 |
Second description
42 |
Third description
43 |
44 | `); 45 | const multipleDescriptions = queryByTestId('multiple'); 46 | 47 | expect(multipleDescriptions).toHaveDescription('First description Second description Third description'); 48 | expect(multipleDescriptions).toHaveDescription(/Second description Third/); 49 | expect(multipleDescriptions).not.toHaveDescription('First'); 50 | expect(multipleDescriptions).not.toHaveDescription('whatever'); 51 | expect(multipleDescriptions).not.toHaveDescription(/stuff/); 52 | }); 53 | 54 | it('normalizes whitespace', () => { 55 | const { queryByTestId } = render(` 56 |
57 | Step 58 | 1 59 | of 60 | 4 61 |
62 |
63 | And 64 | extra 65 | description 66 |
67 |
68 | `); 69 | expect(queryByTestId('target')).toHaveDescription('Step 1 of 4 And extra description'); 70 | expect(queryByTestId('target')).not.toHaveDescription(/\s{2,}/); 71 | }); 72 | 73 | it('can handle multiple levels w/ content spread across descendants', () => { 74 | const { queryByTestId } = render(` 75 | 76 | Step 77 | 1 78 | of 79 | 80 | 81 | 4 82 | 83 |
84 | `); 85 | 86 | expect(queryByTestId('target')).toHaveDescription('Step 1 of 4'); 87 | expect(queryByTestId('target')).not.toHaveDescription('Step'); 88 | }); 89 | 90 | it('handles extra whitespace w/ multiple ids', () => { 91 | const { queryByTestId } = render(` 92 |
First description
93 |
Second description
94 |
Third description
95 |
98 | `); 99 | 100 | expect(queryByTestId('multiple')).toHaveDescription('First description Second description Third description'); 101 | expect(queryByTestId('multiple')).not.toHaveDescription('First description'); 102 | }); 103 | 104 | it('is case-sensitive', () => { 105 | const { queryByTestId } = render(` 106 | Sensitive text 107 |
108 | `); 109 | 110 | expect(queryByTestId('target')).toHaveDescription('Sensitive text'); 111 | expect(queryByTestId('target')).not.toHaveDescription('sensitive text'); 112 | }); 113 | 114 | it('negative test cases', () => { 115 | const { queryByTestId } = render(` 116 |
The description
117 |
118 | `); 119 | const unexistentElement = queryByTestId('other'); 120 | const targetElement = queryByTestId('target'); 121 | const { message: negativeTargetMessage, pass: negativeTargetPass } = negativeCompare( 122 | targetElement, 123 | 'The description' 124 | ); 125 | const { message: positiveTargetMessage, pass: positiveTargetPass } = compare(targetElement, 'whatever'); 126 | 127 | expect(() => compare(unexistentElement, 'The description')).toThrowError( 128 | /FAILED.*Received element must be an HTMLElement or an SVGElement.*/ 129 | ); 130 | 131 | expect(negativeTargetPass).toBeFalse(); 132 | expect(negativeTargetMessage).toMatch(/Expected.*not to have description.*/); 133 | expect(positiveTargetPass).toBeFalse(); 134 | expect(positiveTargetMessage).toMatch(/Expected.*to have description.*/); 135 | expect(targetElement).toHaveDescription(); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/toHaveClassName.js: -------------------------------------------------------------------------------- 1 | import { checkHtmlElement, getTag } from './utils'; 2 | import { printSecSuccess, printSuccess, printSecError, printError, printSecWarning } from './printers'; 3 | 4 | function getExpectedClassNamesAndOptions(params) { 5 | const lastParam = params.pop(); 6 | let expectedClassNames, options; 7 | 8 | if (typeof lastParam === 'object') { 9 | expectedClassNames = params; 10 | options = lastParam; 11 | } else { 12 | expectedClassNames = params.concat(lastParam); 13 | options = { 14 | exact: false, 15 | }; 16 | } 17 | 18 | return { 19 | expectedClassNames, 20 | options, 21 | }; 22 | } 23 | 24 | function splitClassNames(str) { 25 | if (!str) { 26 | return []; 27 | } 28 | 29 | return str.split(/\s+/).filter(s => s.length > 0); 30 | } 31 | 32 | function isSubset(subset, superset) { 33 | return subset.every(item => superset.includes(item)); 34 | } 35 | 36 | export function toHaveClassName() { 37 | return { 38 | compare: function (htmlElement, ...params) { 39 | checkHtmlElement(htmlElement); 40 | let result = {}; 41 | const { expectedClassNames, options } = getExpectedClassNamesAndOptions(params); 42 | const received = splitClassNames(htmlElement.getAttribute('class')); 43 | const expected = expectedClassNames.reduce((acc, className) => acc.concat(splitClassNames(className)), []); 44 | if (options.exact) { 45 | result.pass = isSubset(expected, received) && expected.length === received.length; 46 | result.message = result.pass 47 | ? `${printSuccess('PASSED')} ${printSecSuccess( 48 | `Expected the provided ${printSuccess(getTag(htmlElement))} element to have ${printSuccess( 49 | 'EXACTLY' 50 | )} defined classes ${printSuccess(`${expected.join(' ')}`)}. Received ${printSuccess( 51 | `${received.join(' ')}` 52 | )}.` 53 | )}` 54 | : `${printError('FAILED')} ${printSecError( 55 | `Expected the provided ${printError(getTag(htmlElement))} element to have ${printError( 56 | 'EXACTLY' 57 | )} defined classes ${printError(`${expected.join(' ')}`)}. Received ${printError( 58 | `${received.join(' ')}` 59 | )}.` 60 | )}`; 61 | return result; 62 | } 63 | if (expected.length > 0) { 64 | result.pass = isSubset(expected, received); 65 | result.message = result.pass 66 | ? `${printSuccess('PASSED')} ${printSecSuccess( 67 | ` Expected the provided ${printSuccess(getTag(htmlElement))} element to have class ${printSuccess( 68 | expected.join(' ') 69 | )}. Received ${printSuccess(received.join(' '))}.` 70 | )}` 71 | : `${printError('FAILED')} ${printSecError( 72 | ` Expected the provided ${printError(getTag(htmlElement))} element to have class ${printError( 73 | expected.join(' ') 74 | )}. Received ${printError(received.join(' '))}.` 75 | )}`; 76 | } else { 77 | result.pass = false; 78 | result.message = `${printError('FAILED')} ${printSecWarning(`At least one expected class must be provided.`)}`; 79 | } 80 | return result; 81 | }, 82 | negativeCompare: function (htmlElement, ...params) { 83 | checkHtmlElement(htmlElement); 84 | let result = {}; 85 | const { expectedClassNames, options } = getExpectedClassNamesAndOptions(params); 86 | const received = splitClassNames(htmlElement.getAttribute('class')); 87 | const expected = expectedClassNames.reduce((acc, className) => acc.concat(splitClassNames(className)), []); 88 | if (options.exact) { 89 | result.pass = !isSubset(expected, received) || expected.length !== received.length; 90 | result.message = result.pass 91 | ? `${printSuccess('PASSED')} ${printSecSuccess( 92 | `Expected the provided ${printSuccess(getTag(htmlElement))} element not to have ${printSuccess( 93 | 'EXACTLY' 94 | )} defined classes ${printSuccess(`${expected.join(' ')}`)}. Received ${printSuccess( 95 | `${received.join(' ')}` 96 | )}.` 97 | )}` 98 | : `${printError('FAILED')} ${printSecError( 99 | `Expected the provided ${printError(getTag(htmlElement))} element not to have ${printError( 100 | 'EXACTLY' 101 | )} defined classes ${printError(`${expected.join(' ')}`)}. Received ${printError( 102 | `${received.join(' ')}` 103 | )}.` 104 | )}`; 105 | return result; 106 | } 107 | if (expected.length > 0) { 108 | result.pass = !isSubset(expected, received); 109 | result.message = result.pass 110 | ? `${printSuccess('PASSED')} ${printSecSuccess( 111 | ` Expected the provided ${printSuccess(getTag(htmlElement))} element not to have class ${printSuccess( 112 | expected.join(' ') 113 | )}. Received ${printSuccess(received.join(' '))}.` 114 | )}` 115 | : `${printError('FAILED')} ${printSecError( 116 | ` Expected the provided ${printError(getTag(htmlElement))} element not to have class ${printError( 117 | expected.join(' ') 118 | )}. Received ${printError(received.join(' '))}.` 119 | )}`; 120 | } else { 121 | result.pass = received.length === 0; 122 | result.message = result.pass 123 | ? `${printSuccess('PASSED')} ${printSecSuccess( 124 | `Expected the element not to have classes ${printSuccess('(any)')}.\nReceived: ${printSuccess( 125 | received.join(' ') 126 | )}` 127 | )}` 128 | : `${printError('FAILED')} ${printSecError( 129 | `Expected the element not to have classes ${printError('(any)')}.\nReceived: ${printError( 130 | received.join(' ') 131 | )}` 132 | )}`; 133 | } 134 | return result; 135 | }, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /src/__tests__/toHaveStyle.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import document from './helpers/jsdom'; 3 | import { toHaveStyle } from '../toHaveStyle'; 4 | 5 | describe('.toHaveStyle', () => { 6 | const { compare, negativeCompare } = toHaveStyle(); 7 | 8 | it('positive test cases', () => { 9 | const { container } = render(` 10 |
11 | Hello world 12 |
13 | `); 14 | const style = document.createElement('style'); 15 | style.innerHTML = ` 16 | .label { 17 | align-items: center; 18 | background-color: black; 19 | color: white; 20 | float: left; 21 | transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275); 22 | transform: translateX(0px); 23 | } 24 | `; 25 | document.body.appendChild(style); 26 | document.body.appendChild(container); 27 | 28 | expect(container.querySelector('.label')).toHaveStyle(` 29 | height: 100%; 30 | color: white; 31 | background-color: blue; 32 | `); 33 | 34 | expect(container.querySelector('.label')).toHaveStyle(` 35 | background-color: blue; 36 | color: white; 37 | `); 38 | 39 | expect(container.querySelector('.label')).toHaveStyle( 40 | 'transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275);' 41 | ); 42 | 43 | expect(container.querySelector('.label')).toHaveStyle('background-color:blue;color:white'); 44 | 45 | expect(container.querySelector('.label')).not.toHaveStyle(` 46 | color: white; 47 | font-weight: bold; 48 | `); 49 | 50 | expect(container.querySelector('.label')).toHaveStyle(` 51 | Align-items: center; 52 | `); 53 | 54 | expect(container.querySelector('.label')).toHaveStyle(` 55 | transform: translateX(0px); 56 | `); 57 | }); 58 | 59 | it('normalizes colors accordingly', () => { 60 | const { queryByTestId } = render(` 61 | Hello, world! 62 | `); 63 | 64 | expect(queryByTestId('color-example')).toHaveStyle('background-color: #123456'); 65 | }); 66 | 67 | it('properly normalizes colors for border', () => { 68 | const { queryByTestId } = render(` 69 | Hello World 70 | `); 71 | 72 | expect(queryByTestId('color-example')).toHaveStyle('border: 1px solid #fff'); 73 | }); 74 | 75 | it('handles different formats for color declarations accordingly', () => { 76 | const { queryByTestId } = render(` 77 | Hello, world! 78 | `); 79 | 80 | expect(queryByTestId('color-example')).toHaveStyle('color: #000000'); 81 | expect(queryByTestId('color-example')).toHaveStyle('background-color: rgba(0, 0, 0, 1)'); 82 | }); 83 | 84 | it('handles nonexistent styles accordingly', () => { 85 | const { container } = render(` 86 |
87 | Hello, world! 88 |
89 | `); 90 | 91 | expect(container.querySelector('.label')).not.toHaveStyle('whatever: something;'); 92 | }); 93 | 94 | it('handles styles in JS objects', () => { 95 | const { container } = render(` 96 |
97 | Hello, world! 98 |
99 | `); 100 | 101 | expect(container.querySelector('.label')).toHaveStyle({ 102 | backgroundColor: 'blue', 103 | }); 104 | expect(container.querySelector('.label')).toHaveStyle({ 105 | backgroundColor: 'blue', 106 | height: '100%', 107 | }); 108 | expect(container.querySelector('.label')).not.toHaveStyle({ 109 | backgroundColor: 'red', 110 | height: '100%', 111 | }); 112 | expect(container.querySelector('.label')).not.toHaveStyle({ 113 | whatever: 'anything', 114 | }); 115 | }); 116 | 117 | it('negative test cases', () => { 118 | const { container } = render(` 119 |
120 | Hello world 121 |
122 | `); 123 | const style = document.createElement('style'); 124 | style.innerHTML = ` 125 | .label { 126 | align-items: center; 127 | background-color: black; 128 | color: white; 129 | float: left; 130 | transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275); 131 | transform: translateX(0px); 132 | } 133 | `; 134 | document.body.appendChild(style); 135 | document.body.appendChild(container); 136 | const { message: positiveMessage, pass: positivePass } = compare( 137 | container.querySelector('.label'), 138 | 'align-items: left' 139 | ); 140 | const { message: negativeMessage, pass: negativePass } = negativeCompare( 141 | container.querySelector('.label'), 142 | 'color: white' 143 | ); 144 | 145 | expect(positivePass).toBeFalse(); 146 | expect(positiveMessage).toMatch(/Expected the provided.*element to have styles/); 147 | 148 | expect(negativePass).toBeFalse(); 149 | expect(negativeMessage).toMatch(/Expected the provided.*element not to have styles/); 150 | }); 151 | 152 | it('throws for invalid CSS syntax', () => { 153 | const { container } = render(` 154 |
155 | Hello world 156 |
157 | `); 158 | const style = document.createElement('style'); 159 | style.innerHTML = ` 160 | .label { 161 | align-items: center; 162 | background-color: black; 163 | color: white; 164 | float: left; 165 | transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275); 166 | transform: translateX(0px); 167 | } 168 | `; 169 | document.body.appendChild(style); 170 | document.body.appendChild(container); 171 | 172 | expect(() => expect(container.querySelector('.label')).toHaveStyle('font weigh bold')).toThrowError(); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/__tests__/toHaveDisplayValue.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveDisplayValue } from '../toHaveDisplayValue'; 3 | 4 | describe('.toHaveDisplayValue', () => { 5 | const { compare, negativeCompare } = toHaveDisplayValue(); 6 | 7 | describe('w/ supported elements', () => { 8 | it('positive test cases', () => { 9 | const { queryByTestId } = render(` 10 | 16 | `); 17 | const select = queryByTestId('select'); 18 | 19 | expect(select).toHaveDisplayValue('Select a fruit...'); 20 | expect(select).not.toHaveDisplayValue('Banana'); 21 | 22 | select.value = 'banana'; 23 | 24 | expect(select).toHaveDisplayValue('Banana'); 25 | expect(select).toHaveDisplayValue(/[bB]ana/); 26 | }); 27 | 28 | it('negative test cases', () => { 29 | const { queryByTestId } = render(` 30 | 36 | `); 37 | 38 | const { message: negativeMessage, pass: negativePass } = negativeCompare( 39 | queryByTestId('select'), 40 | 'Select a fruit...' 41 | ); 42 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('select'), 'Ananas'); 43 | 44 | expect(negativePass).toBeFalse(); 45 | expect(negativeMessage).toMatch( 46 | /FAILED.*Expected the element.*select.*not to have display value.*'Select a fruit\.\.\.'.*\. Received.*'Select a fruit\.\.\.'/i 47 | ); 48 | expect(positivePass).toBeFalse(); 49 | expect(positiveMessage).toMatch( 50 | /FAILED.*Expected the element.*select.*to have display value.*'Ananas'.*\. Received.*'Select a fruit\.\.\.'/i 51 | ); 52 | }); 53 | 54 | it('w/ input elements', () => { 55 | const { queryByTestId } = render(` 56 | 57 | `); 58 | const input = queryByTestId('input'); 59 | 60 | expect(input).toHaveDisplayValue('Luca'); 61 | expect(input).toHaveDisplayValue(/Luc/); 62 | expect(input).not.toHaveDisplayValue('Brian'); 63 | 64 | input.value = 'Brian'; 65 | 66 | expect(input).toHaveDisplayValue('Brian'); 67 | expect(input).not.toHaveDisplayValue('Luca'); 68 | }); 69 | 70 | it('w/ textarea elements', () => { 71 | const { queryByTestId } = render(''); 72 | const textarea = queryByTestId('textarea'); 73 | 74 | expect(textarea).toHaveDisplayValue('An example description here.'); 75 | expect(textarea).toHaveDisplayValue(/example/); 76 | expect(textarea).not.toHaveDisplayValue('Another example'); 77 | 78 | textarea.value = 'Another example'; 79 | 80 | expect(textarea).toHaveDisplayValue('Another example'); 81 | expect(textarea).not.toHaveDisplayValue('An example description here.'); 82 | }); 83 | }); 84 | 85 | describe('w/ multiple select', () => { 86 | function mount() { 87 | return render(` 88 | 94 | `); 95 | } 96 | 97 | it('matches only when all the multiple selected values are equal to all the expected values', () => { 98 | const { queryByTestId } = mount(); 99 | const select = queryByTestId('select'); 100 | 101 | expect(select).toHaveDisplayValue(['Ananas', 'Avocado']); 102 | expect(select).not.toHaveDisplayValue(['Ananas', 'Avocado', 'Orange']); 103 | expect(select).not.toHaveDisplayValue('Ananas'); 104 | Array.from(select.options).forEach(option => { 105 | option.selected = ['ananas', 'banana'].includes(option.value); 106 | }); 107 | 108 | expect(select).toHaveDisplayValue(['Ananas', 'Banana']); 109 | }); 110 | 111 | it('matches even when the expected values are unordered', () => { 112 | const { queryByTestId } = mount(); 113 | 114 | expect(queryByTestId('select')).toHaveDisplayValue(['Avocado', 'Ananas']); 115 | }); 116 | 117 | it('matches with RegExp expected values', () => { 118 | const { queryByTestId } = mount(); 119 | 120 | expect(queryByTestId('select')).toHaveDisplayValue([/[Aa]nanas/, 'Avocado']); 121 | }); 122 | }); 123 | 124 | describe('w/ invalid elements', () => { 125 | const { queryByTestId } = render(` 126 |
Banana
127 | 128 | 129 | `); 130 | it('should throw', () => { 131 | let errorMessage; 132 | 133 | try { 134 | expect(queryByTestId('div')).toHaveDisplayValue('Banana'); 135 | } catch (err) { 136 | errorMessage = err.message; 137 | } 138 | 139 | expect(errorMessage).toMatch(/\.toHaveDisplayValue\(\) supports only.*input.*textarea.*select.*/); 140 | 141 | try { 142 | expect(queryByTestId('div')).not.toHaveDisplayValue('Banana'); 143 | } catch (err) { 144 | errorMessage = err.message; 145 | } 146 | 147 | expect(errorMessage).toMatch(/\.toHaveDisplayValue\(\) supports only.*input.*textarea.*select.*/); 148 | 149 | try { 150 | expect(queryByTestId('radio')).toHaveDisplayValue('whatever'); 151 | } catch (err) { 152 | errorMessage = err.message; 153 | } 154 | 155 | expect(errorMessage).toMatch(/\.toHaveDisplayValue\(\) currently does not support.*input\[type="radio"\].*/); 156 | 157 | try { 158 | expect(queryByTestId('checkbox')).toHaveDisplayValue(true); 159 | } catch (err) { 160 | errorMessage = err.message; 161 | } 162 | 163 | expect(errorMessage).toMatch(/\.toHaveDisplayValue\(\) currently does not support.*input\[type="checkbox"\].*/); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /src/__tests__/toBePartiallyChecked.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBePartiallyChecked } from '../toBePartiallyChecked'; 3 | 4 | describe('.toBePartiallyChecked', () => { 5 | const { compare, negativeCompare } = toBePartiallyChecked(); 6 | 7 | it('input type checkbox w/ aria-checked', () => { 8 | const { queryByTestId } = render(` 9 | 10 | 11 | 12 | `); 13 | 14 | expect(queryByTestId('mixed-checkbox')).toBePartiallyChecked(); 15 | expect(queryByTestId('checked-checkbox')).not.toBePartiallyChecked(); 16 | expect(queryByTestId('unchecked-checkbox')).not.toBePartiallyChecked(); 17 | }); 18 | 19 | it('input type checkbox w/ indeterminate set to true', () => { 20 | const { queryByTestId } = render(` 21 | 22 | 23 | 24 | `); 25 | queryByTestId('mixed-checkbox').indeterminate = true; 26 | 27 | expect(queryByTestId('mixed-checkbox')).toBePartiallyChecked(); 28 | expect(queryByTestId('checked-checkbox')).not.toBePartiallyChecked(); 29 | expect(queryByTestId('unchecked-checkbox')).not.toBePartiallyChecked(); 30 | }); 31 | 32 | it('elements w/ checkbox role', () => { 33 | const { queryByTestId } = render(` 34 |
35 |
36 |
37 | `); 38 | 39 | expect(queryByTestId('aria-checkbox-mixed')).toBePartiallyChecked(); 40 | expect(queryByTestId('aria-checkbox-checked')).not.toBePartiallyChecked(); 41 | expect(queryByTestId('aria-checkbox-unchecked')).not.toBePartiallyChecked(); 42 | }); 43 | 44 | it('throws when input type checkbox is mixed but expected not to be', () => { 45 | const { queryByTestId } = render(` 46 | 47 | `); 48 | const { message, pass } = negativeCompare(queryByTestId('mixed-checkbox')); 49 | 50 | expect(pass).toBeFalse(); 51 | expect(message).toMatch(/Expected the element.*not to be partially checked, and it.*is partially checked.*\./); 52 | }); 53 | 54 | it('throws when input type checkbox is indeterminate but expected not to be', () => { 55 | const { queryByTestId } = render(` 56 | 57 | `); 58 | queryByTestId('mixed-checkbox').indeterminate = true; 59 | const { message, pass } = negativeCompare(queryByTestId('mixed-checkbox')); 60 | 61 | expect(pass).toBeFalse(); 62 | expect(message).toMatch(/Expected the element.*not to be partially checked, and it.*is partially checked.*\./); 63 | }); 64 | 65 | it('throws when input type checkbox is not checked but expected to be', () => { 66 | const { queryByTestId } = render(` 67 | 68 | `); 69 | const { message, pass } = compare(queryByTestId('empty-checkbox')); 70 | 71 | expect(pass).toBeFalse(); 72 | expect(message).toMatch(/Expected the element.*to be partially checked, and it.*isn't partially checked.*\./); 73 | }); 74 | 75 | it('throws when element with checkbox role is partially checked but expected not to be', () => { 76 | const { queryByTestId } = render(` 77 |
78 | `); 79 | const { message, pass } = negativeCompare(queryByTestId('mixed-aria-checkbox')); 80 | 81 | expect(pass).toBeFalse(); 82 | expect(message).toMatch(/Expected the element.*not to be partially checked, and it.*is partially checked.*\./); 83 | }); 84 | 85 | it('throws when element with checkbox role is checked but expected to be partially checked', () => { 86 | const { queryByTestId } = render(` 87 |
88 | `); 89 | const { message, pass } = compare(queryByTestId('checked-aria-checkbox')); 90 | 91 | expect(pass).toBeFalse(); 92 | expect(message).toMatch(/Expected the element.*to be partially checked, and it.*isn't partially checked.*\./); 93 | }); 94 | 95 | it("throws when element with checkbox role isn't checked but expected to be partially checked", () => { 96 | const { queryByTestId } = render(` 97 |
98 | `); 99 | const { message, pass } = compare(queryByTestId('unchecked-aria-checkbox')); 100 | 101 | expect(pass).toBeFalse(); 102 | expect(message).toMatch(/Expected the element.*to be partially checked, and it.*isn't partially checked.*\./); 103 | }); 104 | 105 | it('throws when element with checkbox role has an invalid aria-checked attribute', () => { 106 | const { queryByTestId } = render(` 107 |
108 | `); 109 | const { message, pass } = compare(queryByTestId('invalid-aria-checkbox')); 110 | 111 | expect(pass).toBeFalse(); 112 | expect(message).toMatch(/Expected the element.*to be partially checked, and it.*isn't partially checked.*\./); 113 | }); 114 | 115 | it('throws when the element is not a valid checkbox', () => { 116 | const { queryByTestId } = render(` 117 | 118 | `); 119 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('select')); 120 | const { message: negativeMessage, pass: negativePass } = negativeCompare(queryByTestId('select')); 121 | 122 | expect(positivePass).toBeFalse(); 123 | expect(positiveMessage).toMatch( 124 | /Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with.*toBePartiallyChecked\(\).*Use.*toHaveValue\(\).*instead/ 125 | ); 126 | 127 | expect(negativePass).toBeFalse(); 128 | expect(negativeMessage).toMatch( 129 | /Only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with.*toBePartiallyChecked\(\).*Use.*toHaveValue\(\).*instead/ 130 | ); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/__tests__/toHaveValue.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveValue } from '../toHaveValue'; 3 | 4 | describe('.toHaveValue', () => { 5 | const { compare, negativeCompare } = toHaveValue(); 6 | it('handles text input', () => { 7 | const { queryByTestId } = render(` 8 | 9 | 10 | 11 | `); 12 | const text = queryByTestId('text'); 13 | const empty = queryByTestId('empty'); 14 | const noValue = queryByTestId('without'); 15 | 16 | expect(text).toHaveValue('foo'); 17 | expect(text).toHaveValue(); 18 | expect(text).not.toHaveValue('bar'); 19 | expect(text).not.toHaveValue(''); 20 | 21 | expect(empty).toHaveValue(''); 22 | expect(empty).not.toHaveValue('foo'); 23 | expect(empty).not.toHaveValue(); 24 | 25 | expect(noValue).toHaveValue(''); 26 | expect(noValue).not.toHaveValue(); 27 | expect(noValue).not.toHaveValue('foo'); 28 | noValue.value = 'bar'; 29 | expect(noValue).toHaveValue('bar'); 30 | }); 31 | 32 | it('handles number input', () => { 33 | const { queryByTestId } = render(` 34 | 35 | 36 | 37 | `); 38 | const number = queryByTestId('number'); 39 | const empty = queryByTestId('empty'); 40 | const noValue = queryByTestId('without'); 41 | 42 | expect(number).toHaveValue(5); 43 | expect(number).toHaveValue(); 44 | expect(number).not.toHaveValue(4); 45 | expect(number).not.toHaveValue('5'); 46 | 47 | expect(empty).toHaveValue(null); 48 | expect(empty).not.toHaveValue('5'); 49 | expect(empty).not.toHaveValue(); 50 | 51 | expect(noValue).toHaveValue(null); 52 | expect(noValue).not.toHaveValue(); 53 | expect(noValue).not.toHaveValue('10'); 54 | noValue.value = 10; 55 | expect(noValue).toHaveValue(10); 56 | 57 | const { message: positiveMessage, pass: positivePass } = compare(number, '5'); 58 | const { message: negativeMessage, pass: negativePass } = negativeCompare(number); 59 | 60 | expect(positivePass).toBeFalse(); 61 | expect(positiveMessage).toMatch(/Expected the provided.*input.*to have value.*5 \(string\).*\./); 62 | 63 | expect(negativePass).toBeFalse(); 64 | expect(negativeMessage).toMatch(/Expected the provided.*input.*not to have value.*\(any\).*\./); 65 | }); 66 | 67 | it('handles select element', () => { 68 | const { queryByTestId } = render(` 69 | 74 | 75 | 80 | 81 | 87 | `); 88 | const single = queryByTestId('single'); 89 | const multiple = queryByTestId('multiple'); 90 | const notSelected = queryByTestId('not-selected'); 91 | 92 | expect(single).toHaveValue('second'); 93 | expect(single).toHaveValue(); 94 | 95 | expect(multiple).toHaveValue(['second', 'third']); 96 | expect(multiple).toHaveValue(); 97 | 98 | expect(notSelected).not.toHaveValue(); 99 | expect(notSelected).toHaveValue(''); 100 | 101 | single.children[0].setAttribute('selected', true); 102 | expect(single).toHaveValue('first'); 103 | }); 104 | 105 | it('handles textarea element', () => { 106 | const { queryByTestId } = render(` 107 | 108 | `); 109 | expect(queryByTestId('textarea')).toHaveValue('text value'); 110 | }); 111 | 112 | it('throws when passed checkbox or radio inputs', () => { 113 | const { queryByTestId } = render(` 114 | 115 | 116 | `); 117 | let errorMsg; 118 | 119 | try { 120 | expect(queryByTestId('checkbox')).toHaveValue(''); 121 | } catch (error) { 122 | errorMsg = error.message; 123 | } 124 | expect(errorMsg).toMatch( 125 | /input elements with.*type="checkbox\/radio".*cannot be used with.*\.toHaveValue\(\).*\. Use.*\.toBeChecked\(\).*for type="checkbox" or.*\.toHaveFormValues\(\).*instead\./ 126 | ); 127 | 128 | try { 129 | expect(queryByTestId('radio')).not.toHaveValue(''); 130 | } catch (error) { 131 | errorMsg = error.message; 132 | } 133 | expect(errorMsg).toMatch( 134 | /input elements with.*type="checkbox\/radio".*cannot be used with.*\.toHaveValue\(\).*\. Use.*\.toBeChecked\(\).*for type="checkbox" or.*\.toHaveFormValues\(\).*instead\./ 135 | ); 136 | }); 137 | 138 | it('throws when the expected input value does not match', () => { 139 | const { queryByTestId } = render(` 140 | 141 | `); 142 | const { message, pass } = compare(queryByTestId('one'), 'whatever'); 143 | 144 | expect(pass).toBeFalse(); 145 | expect(message).toMatch(/Expected the provided.*input.*to have value.*whatever.*/); 146 | }); 147 | 148 | it('throws with type information when the expected text input value has loose equality with the received value', () => { 149 | const { queryByTestId } = render(` 150 | 151 | 152 | `); 153 | const { message: noValueMessage, pass: noValuePass } = compare(queryByTestId('two')); 154 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('one'), 8); 155 | const { message: negativeMessage, pass: negativePass } = negativeCompare(queryByTestId('one'), '8'); 156 | 157 | expect(noValuePass).toBeFalse(); 158 | expect(noValueMessage).toMatch(/Expected the provided.*input.*to have value.*\(any\).*\./); 159 | 160 | expect(positivePass).toBeFalse(); 161 | expect(positiveMessage).toMatch(/Expected the provided.*input.*to have value.*8 \(number\).*\./); 162 | 163 | expect(negativePass).toBeFalse(); 164 | expect(negativeMessage).toMatch(/Expected the provided.*input.*not to have value.*8.*\./); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /src/__tests__/toContainHTML.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toContainHTML } from '../toContainHTML'; 3 | import { printSuccess, printSecError, printError } from '../printers'; 4 | 5 | /** 6 | * @param {object} params 7 | * @param {boolean} params.positivePass 8 | * @param {RegExp} params.positiveMatch 9 | * @param {RegExp} params.negativeMatch 10 | * @param {Element} params.htmlElement 11 | * @param {string} params.htmlText 12 | */ 13 | function testPositiveNagative({ positivePass, positiveMatch, negativeMatch, htmlElement, htmlText }) { 14 | const { compare, negativeCompare } = toContainHTML(); 15 | 16 | const positive = compare(htmlElement, htmlText); 17 | expect(positive.pass)[positivePass ? 'toBeTrue' : 'toBeFalse'](); 18 | expect(positive.message).toMatch(positiveMatch); 19 | 20 | const negative = negativeCompare(htmlElement, htmlText); 21 | expect(negative.pass)[positivePass ? 'toBeFalse' : 'toBeTrue'](); 22 | expect(negative.message).toMatch(negativeMatch); 23 | } 24 | 25 | /* eslint-disable max-statements */ 26 | describe('.toContainHTML', () => { 27 | it('handles positive and negative cases', () => { 28 | const { queryByTestId } = render(` 29 | 30 | 31 | 32 | 33 | 34 | 35 | `); 36 | 37 | const grandparent = queryByTestId('grandparent'); 38 | const parent = queryByTestId('parent'); 39 | const child = queryByTestId('child'); 40 | const nonExistantElement = queryByTestId('not-exists'); 41 | const fakeElement = { thisIsNot: 'an html element' }; 42 | const stringChildElement = ''; 43 | const stringChildElementSelfClosing = ''; 44 | const incorrectStringHtml = '
'; 45 | const nonExistantString = ' Does not exists '; 46 | const svgElement = queryByTestId('svg-element'); 47 | 48 | expect(grandparent).toContainHTML(stringChildElement); 49 | expect(parent).toContainHTML(stringChildElement); 50 | expect(child).toContainHTML(stringChildElement); 51 | expect(child).toContainHTML(stringChildElementSelfClosing); 52 | expect(grandparent).not.toContainHTML(nonExistantString); 53 | expect(parent).not.toContainHTML(nonExistantString); 54 | expect(child).not.toContainHTML(nonExistantString); 55 | expect(child).not.toContainHTML(nonExistantString); 56 | expect(grandparent).toContainHTML(incorrectStringHtml); 57 | expect(parent).toContainHTML(incorrectStringHtml); 58 | expect(child).toContainHTML(incorrectStringHtml); 59 | 60 | // Tests that throws 61 | expect(() => expect(nonExistantElement).toContainHTML(stringChildElement)).toThrowError(); 62 | expect(() => expect(nonExistantElement).toContainHTML(nonExistantElement)).toThrowError(); 63 | expect(() => expect(stringChildElement).toContainHTML(fakeElement)).toThrowError(); 64 | expect(() => expect(nonExistantElement).not.toContainHTML(stringChildElement)).toThrowError(); 65 | expect(() => expect(nonExistantElement).not.toContainHTML(nonExistantElement)).toThrowError(); 66 | expect(() => expect(stringChildElement).not.toContainHTML(fakeElement)).toThrowError(); 67 | expect(() => expect(nonExistantElement).not.toContainHTML(incorrectStringHtml)).toThrowError(); 68 | 69 | // negative test cases wrapped in throwError assertions for coverage. 70 | testPositiveNagative({ 71 | positivePass: false, 72 | positiveMatch: /Expected:.*Received:.*/, 73 | negativeMatch: /Expected:.*Received:.*/, 74 | htmlElement: svgElement, 75 | htmlText: stringChildElement, 76 | }); 77 | 78 | testPositiveNagative({ 79 | positivePass: true, 80 | positiveMatch: /Expected:.*Received:.*/, 81 | negativeMatch: /Expected:.*Received:.*/, 82 | htmlElement: parent, 83 | htmlText: stringChildElement, 84 | }); 85 | 86 | testPositiveNagative({ 87 | positivePass: true, 88 | positiveMatch: /Expected:.*Received:.*/, 89 | negativeMatch: /Expected:.*Received:.*/, 90 | htmlElement: grandparent, 91 | htmlText: stringChildElement, 92 | }); 93 | 94 | testPositiveNagative({ 95 | positivePass: true, 96 | positiveMatch: /Expected:.*Received:.*/, 97 | negativeMatch: /Expected:.*Received:.*/, 98 | htmlElement: child, 99 | htmlText: stringChildElement, 100 | }); 101 | 102 | testPositiveNagative({ 103 | positivePass: true, 104 | positiveMatch: /Expected:.*Received:.*/, 105 | negativeMatch: /Expected:.*Received:.*/, 106 | htmlElement: child, 107 | htmlText: stringChildElementSelfClosing, 108 | }); 109 | 110 | testPositiveNagative({ 111 | positivePass: false, 112 | positiveMatch: /Expected:.*Received:.*/, 113 | negativeMatch: /Expected:.*Received:.*/, 114 | htmlElement: child, 115 | htmlText: nonExistantString, 116 | }); 117 | 118 | testPositiveNagative({ 119 | positivePass: false, 120 | positiveMatch: /Expected:.*Received:.*/, 121 | negativeMatch: /Expected:.*Received:.*/, 122 | htmlElement: parent, 123 | htmlText: nonExistantString, 124 | }); 125 | 126 | testPositiveNagative({ 127 | positivePass: false, 128 | positiveMatch: /Expected:.*Received:.*/, 129 | negativeMatch: /Expected:.*Received:.*/, 130 | htmlElement: grandparent, 131 | htmlText: nonExistantString, 132 | }); 133 | 134 | testPositiveNagative({ 135 | positivePass: true, 136 | positiveMatch: /Expected:.*Received:.*/, 137 | negativeMatch: /Expected:.*Received:.*/, 138 | htmlElement: grandparent, 139 | htmlText: incorrectStringHtml, 140 | }); 141 | 142 | testPositiveNagative({ 143 | positivePass: true, 144 | positiveMatch: /Expected:.*Received:.*/, 145 | negativeMatch: /Expected:.*Received:.*/, 146 | htmlElement: child, 147 | htmlText: incorrectStringHtml, 148 | }); 149 | 150 | testPositiveNagative({ 151 | positivePass: true, 152 | positiveMatch: /Expected:.*Received:.*/, 153 | negativeMatch: /Expected:.*Received:.*/, 154 | htmlElement: parent, 155 | htmlText: incorrectStringHtml, 156 | }); 157 | }); 158 | 159 | it('throws with an expected text', () => { 160 | const { queryByTestId } = render(''); 161 | const htmlElement = queryByTestId('child'); 162 | const nonExistantString = '
non-existant element
'; 163 | 164 | const errorMessage = toContainHTML().compare(htmlElement, nonExistantString).message; 165 | 166 | expect(errorMessage).toBe( 167 | `${printError('FAILED')} ${printSecError( 168 | `Expected: ${printError(`'
non-existant element
'`)}. Received: ${printSuccess( 169 | '' 170 | )}` 171 | )}` 172 | ); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/__tests__/toBeEnabled.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeEnabled } from '../toBeDisabled'; 3 | 4 | const { compare, negativeCompare } = toBeEnabled(); 5 | 6 | describe('.toBeEnabled()', () => { 7 | const { queryByTestId } = render(` 8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | 21 | 26 |
27 | Nested anchor 28 |
29 | Anchor 30 |
31 | `); 32 | 33 | it('positive test cases', () => { 34 | expect(queryByTestId('div')).toBeEnabled(); 35 | expect(queryByTestId('div-child')).toBeEnabled(); 36 | expect(queryByTestId('button')).not.toBeEnabled(); 37 | }); 38 | 39 | it('negative test cases', () => { 40 | const { message: buttonMessage, pass: buttonPass } = compare(queryByTestId('button')); 41 | const { message: textareaMessage, pass: textareaPass } = compare(queryByTestId('textarea')); 42 | const { message: inputMessage, pass: inputPass } = compare(queryByTestId('input')); 43 | const { message: fieldsetMessage, pass: fieldsetPass } = compare(queryByTestId('fieldset')); 44 | const { message: fieldsetChildMessage, pass: fieldsetChildPass } = compare(queryByTestId('fieldset-child')); 45 | const { message: nestedButtonMessage, pass: nestedButtonPass } = compare(queryByTestId('nested-button')); 46 | const { message: nestedSelectMessage, pass: nestedSelectPass } = compare(queryByTestId('nested-select')); 47 | const { message: nestedOptGroupMessage, pass: nestedOptGroupPass } = compare(queryByTestId('nested-optgroup')); 48 | const { message: nestedOptionMessage, pass: nestedOptionPass } = compare(queryByTestId('nested-option')); 49 | const { message: anchorMessage, pass: anchorPass } = negativeCompare(queryByTestId('anchor')); 50 | const { message: nestedAnchorMessage, pass: nestedAnchorPass } = negativeCompare(queryByTestId('nested-anchor')); 51 | 52 | expect(buttonPass).toBeFalse(); 53 | expect(buttonMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 54 | 55 | expect(textareaPass).toBeFalse(); 56 | expect(textareaMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 57 | 58 | expect(inputPass).toBeFalse(); 59 | expect(inputMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 60 | 61 | expect(fieldsetPass).toBeFalse(); 62 | expect(fieldsetMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 63 | 64 | expect(fieldsetChildPass).toBeFalse(); 65 | expect(fieldsetChildMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 66 | 67 | expect(nestedButtonPass).toBeFalse(); 68 | expect(nestedButtonMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 69 | 70 | expect(nestedSelectPass).toBeFalse(); 71 | expect(nestedSelectMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 72 | 73 | expect(nestedOptGroupPass).toBeFalse(); 74 | expect(nestedOptGroupMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 75 | 76 | expect(nestedOptionPass).toBeFalse(); 77 | expect(nestedOptionMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 78 | 79 | expect(anchorPass).toBeFalse(); 80 | expect(anchorMessage).toMatch(/Expected the element.*not to be enabled and it.*is enabled.*\./); 81 | 82 | expect(nestedAnchorPass).toBeFalse(); 83 | expect(nestedAnchorMessage).toMatch(/Expected the element.*not to be enabled and it.*is enabled.*\./); 84 | }); 85 | }); 86 | 87 | describe('.toBeEnabled() w/ fieldset>legend', () => { 88 | const { queryByTestId } = render(` 89 |
90 |
91 | 92 |
93 |
94 | 95 | 96 | 97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 | 117 | 118 | 119 |
120 |
121 |
122 | `); 123 | 124 | it('positive test cases', () => { 125 | expect(queryByTestId('inside-legend')).toBeEnabled(); 126 | expect(queryByTestId('nested-inside-legend')).toBeEnabled(); 127 | expect(queryByTestId('first-legend-child')).toBeEnabled(); 128 | }); 129 | 130 | it('negative test cases', () => { 131 | const { message: inheritedMessage, pass: inheritedPass } = compare(queryByTestId('inherited')); 132 | const { message: secondChildMessage, pass: secondChildPass } = compare(queryByTestId('second-legend-child')); 133 | const { message: outerFieldsetMessage, pass: outerFieldsetPass } = compare(queryByTestId('outer-fieldset')); 134 | 135 | expect(inheritedPass).toBeFalse(); 136 | expect(inheritedMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 137 | 138 | expect(secondChildPass).toBeFalse(); 139 | expect(secondChildMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 140 | 141 | expect(outerFieldsetPass).toBeFalse(); 142 | expect(outerFieldsetMessage).toMatch(/Expected the element.*to be enabled and it.*isn't enabled.*\./); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/__tests__/toHaveErrorMessage.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveErrorMessage } from '../toHaveErrorMessage'; 3 | 4 | describe('.toHaveErrorMessage', () => { 5 | const matchersUtilMock = { 6 | equals: Object.is, 7 | }; 8 | 9 | const { compare } = toHaveErrorMessage(matchersUtilMock); 10 | 11 | it('resolves for object with correct aria-errormessage reference', () => { 12 | const { queryByTestId } = render(` 13 | 14 | 15 | Invalid time: the time must be between 9:00 AM and 5:00 PM 16 | `); 17 | 18 | const timeInput = queryByTestId('startTime'); 19 | 20 | expect(timeInput).toHaveErrorMessage('Invalid time: the time must be between 9:00 AM and 5:00 PM'); 21 | expect(timeInput).toHaveErrorMessage(/invalid time/i); // to partially match 22 | expect(timeInput).toHaveErrorMessage(jasmine.stringContaining('Invalid time')); // to partially match 23 | expect(timeInput).not.toHaveErrorMessage('Pikachu!'); 24 | }); 25 | 26 | it('works correctly on implicit invalid element', () => { 27 | const { queryByTestId } = render(` 28 | 29 | 30 | Invalid time: the time must be between 9:00 AM and 5:00 PM 31 | `); 32 | 33 | const timeInput = queryByTestId('startTime'); 34 | 35 | expect(timeInput).toHaveErrorMessage('Invalid time: the time must be between 9:00 AM and 5:00 PM'); 36 | expect(timeInput).toHaveErrorMessage(/invalid time/i); // to partially match 37 | expect(timeInput).toHaveErrorMessage(jasmine.stringContaining('Invalid time')); // to partially match 38 | expect(timeInput).not.toHaveErrorMessage('Pikachu!'); 39 | }); 40 | 41 | it('rejects for valid object', () => { 42 | const { queryByTestId } = render(` 43 |
The errormessage
44 |
45 |
46 | `); 47 | 48 | expect(queryByTestId('valid')).not.toHaveErrorMessage('The errormessage'); 49 | expect(compare(queryByTestId('valid'), 'The errormessage').pass).toBeFalse(); 50 | 51 | expect(queryByTestId('explicitly_valid')).not.toHaveErrorMessage('The errormessage'); 52 | expect(compare(queryByTestId('explicitly_valid'), 'The errormessage').pass).toBeFalse(); 53 | }); 54 | 55 | it('rejects for object with incorrect aria-errormessage reference', () => { 56 | const { queryByTestId } = render(` 57 |
The errormessage
58 |
59 | `); 60 | 61 | expect(queryByTestId('invalid_id')).not.toHaveErrorMessage(); 62 | expect(queryByTestId('invalid_id')).toHaveErrorMessage(''); 63 | }); 64 | 65 | it('handles invalid element without aria-errormessage', () => { 66 | const { queryByTestId } = render(` 67 |
The errormessage
68 |
69 | `); 70 | 71 | expect(queryByTestId('without')).not.toHaveErrorMessage(); 72 | expect(queryByTestId('without')).toHaveErrorMessage(''); 73 | }); 74 | 75 | it('handles valid element without aria-errormessage', () => { 76 | const { queryByTestId } = render(` 77 |
The errormessage
78 |
79 | `); 80 | 81 | expect(queryByTestId('without')).not.toHaveErrorMessage(); 82 | expect(compare(queryByTestId('without')).pass).toBeFalse(); 83 | 84 | expect(queryByTestId('without')).not.toHaveErrorMessage(''); 85 | expect(compare(queryByTestId('without', '')).pass).toBeFalse(); 86 | }); 87 | 88 | it('handles multiple ids', () => { 89 | const { queryByTestId } = render(` 90 |
First errormessage
91 |
Second errormessage
92 |
Third errormessage
93 |
94 | `); 95 | 96 | expect(queryByTestId('multiple')).toHaveErrorMessage('First errormessage Second errormessage Third errormessage'); 97 | expect(queryByTestId('multiple')).toHaveErrorMessage(/Second errormessage Third/); 98 | expect(queryByTestId('multiple')).toHaveErrorMessage(jasmine.stringContaining('Second errormessage Third')); 99 | expect(queryByTestId('multiple')).toHaveErrorMessage(jasmine.stringMatching(/Second errormessage Third/)); 100 | expect(queryByTestId('multiple')).not.toHaveErrorMessage('Something else'); 101 | expect(queryByTestId('multiple')).not.toHaveErrorMessage('First'); 102 | }); 103 | 104 | it('handles negative test cases', () => { 105 | const { queryByTestId } = render(` 106 |
The errormessage
107 |
108 | `); 109 | 110 | expect(() => expect(queryByTestId('other')).toHaveErrorMessage('The errormessage')).toThrowError(); 111 | 112 | expect(compare(queryByTestId('target'), 'Something else').pass).toBeFalse(); 113 | 114 | expect(compare(queryByTestId('target'), 'The errormessage').pass).toBeTrue(); 115 | }); 116 | 117 | it('normalizes whitespace', () => { 118 | const { queryByTestId } = render(` 119 |
120 | Step 121 | 1 122 | of 123 | 4 124 |
125 |
126 | And 127 | extra 128 | errormessage 129 |
130 |
131 | `); 132 | 133 | expect(queryByTestId('target')).toHaveErrorMessage('Step 1 of 4 And extra errormessage'); 134 | }); 135 | 136 | it('can handle multiple levels with content spread across decendants', () => { 137 | const { queryByTestId } = render(` 138 | 139 | Step 140 | 1 141 | of 142 | 4 143 | 144 |
145 | `); 146 | 147 | expect(queryByTestId('target')).toHaveErrorMessage('Step 1 of 4'); 148 | }); 149 | 150 | it('handles extra whitespace with multiple ids', () => { 151 | const { queryByTestId } = render(` 152 |
First errormessage
153 |
Second errormessage
154 |
Third errormessage
155 |
158 | `); 159 | 160 | expect(queryByTestId('multiple')).toHaveErrorMessage('First errormessage Second errormessage Third errormessage'); 161 | }); 162 | 163 | it('is case-sensitive', () => { 164 | const { queryByTestId } = render(` 165 | Sensitive text 166 |
167 | `); 168 | 169 | expect(queryByTestId('target')).toHaveErrorMessage('Sensitive text'); 170 | expect(queryByTestId('target')).not.toHaveErrorMessage('sensitive text'); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /src/__tests__/toBeVisible.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeVisible } from '../toBeVisible'; 3 | 4 | describe('.toBeVisible', () => { 5 | const { compare, negativeCompare } = toBeVisible(); 6 | 7 | describe('returns the visibility of an element', () => { 8 | const { container } = render(` 9 |
10 |
11 |

Main title

12 |

Secondary title

13 |

Secondary title

14 |

Secondary title

15 |
Secondary title
16 |
17 | 18 |
19 |

Hello World

20 |
21 |
22 | `); 23 | 24 | it('positive test cases', () => { 25 | expect(container.querySelector('header')).toBeVisible(); 26 | expect(container.querySelector('h1')).not.toBeVisible(); 27 | expect(container.querySelector('h2')).not.toBeVisible(); 28 | expect(container.querySelector('h3')).not.toBeVisible(); 29 | expect(container.querySelector('h4')).not.toBeVisible(); 30 | expect(container.querySelector('h5')).toBeVisible(); 31 | expect(container.querySelector('button')).not.toBeVisible(); 32 | expect(container.querySelector('strong')).not.toBeVisible(); 33 | }); 34 | 35 | it('negative test cases', () => { 36 | const { message: headerMessage, pass: headerPass } = negativeCompare(container.querySelector('header')); 37 | const { message: pMessage, pass: pPass } = compare(container.querySelector('p')); 38 | 39 | expect(headerPass).toBeFalse(); 40 | expect(headerMessage).toMatch(/Expected.*not to be visible.*is visible/); 41 | 42 | expect(pPass).toBeFalse(); 43 | expect(pMessage).toMatch(/Expected.*to be visible.*isn't visible/); 44 | }); 45 | }); 46 | 47 | describe('with a
element', () => { 48 | let subject; 49 | 50 | afterEach(() => { 51 | subject = undefined; 52 | }); 53 | 54 | describe('when the details is opened', () => { 55 | beforeEach(() => { 56 | subject = render(` 57 |
58 | Title of visible 59 |
Visible details
60 |
61 | `); 62 | }); 63 | 64 | it('returns true to the details content', () => { 65 | expect(subject.container.querySelector('div')).toBeVisible(); 66 | }); 67 | 68 | it('returns true to the most inner details content', () => { 69 | expect(subject.container.querySelector('small')).toBeVisible(); 70 | }); 71 | 72 | it('returns true to the details summary', () => { 73 | expect(subject.container.querySelector('small')).toBeVisible(); 74 | }); 75 | 76 | describe('when the user clicks on the summary', () => { 77 | beforeEach(() => { 78 | subject.container.querySelector('summary').click(); 79 | }); 80 | 81 | it('returns false to the details content', () => { 82 | expect(subject.container.querySelector('div')).not.toBeVisible(); 83 | }); 84 | 85 | it('returns true to the details summary', () => { 86 | expect(subject.container.querySelector('summary')).toBeVisible(); 87 | }); 88 | }); 89 | }); 90 | 91 | describe("when the details isn't opened", () => { 92 | beforeEach(() => { 93 | subject = render(` 94 |
95 | Title of hidden 96 |
Hidden details
97 |
98 | `); 99 | }); 100 | 101 | it('returns false to the details content', () => { 102 | expect(subject.container.querySelector('div')).not.toBeVisible(); 103 | }); 104 | 105 | it('returns true to the summary content', () => { 106 | expect(subject.container.querySelector('summary')).toBeVisible(); 107 | }); 108 | 109 | describe('when the user clicks on the summary', () => { 110 | beforeEach(() => { 111 | subject.container.querySelector('summary').click(); 112 | }); 113 | 114 | it('returns true to the details content', () => { 115 | expect(subject.container.querySelector('div')).toBeVisible(); 116 | }); 117 | 118 | it('returns true to the details summary', () => { 119 | expect(subject.container.querySelector('summary')).toBeVisible(); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('when the details is opened but it is hidden', () => { 125 | beforeEach(() => { 126 | subject = render(` 127 | 131 | `); 132 | }); 133 | 134 | it('returns false to the details content', () => { 135 | expect(subject.container.querySelector('div')).not.toBeVisible(); 136 | }); 137 | 138 | it('returns false to the details summary', () => { 139 | expect(subject.container.querySelector('summary')).not.toBeVisible(); 140 | }); 141 | }); 142 | 143 | describe('with a nested
element', () => { 144 | describe('when the nested details is opened', () => { 145 | beforeEach(() => { 146 | subject = render(` 147 |
148 | Title of visible 149 |
Outer content
150 |
151 | Title of nested details 152 |
Inner content
153 |
154 |
155 | `); 156 | }); 157 | 158 | it('returns true to the nested details content', () => { 159 | expect(subject.container.querySelector('details > details > div')).toBeVisible(); 160 | }); 161 | 162 | it('returns true to the nested details summary', () => { 163 | expect(subject.container.querySelector('details > details > summary')).toBeVisible(); 164 | }); 165 | 166 | it('returns true to the outer details content', () => { 167 | expect(subject.container.querySelector('details > div')).toBeVisible(); 168 | }); 169 | 170 | it('returns true to the outer details summary', () => { 171 | expect(subject.container.querySelector('details > summary')).toBeVisible(); 172 | }); 173 | }); 174 | 175 | describe("when the nested details isn't opened", () => { 176 | beforeEach(() => { 177 | subject = render(` 178 |
179 | Title of visible 180 |
Outer content
181 |
182 | Title of nested details 183 |
Inner content
184 |
185 |
186 | `); 187 | }); 188 | 189 | it('returns false to the nested details content', () => { 190 | expect(subject.container.querySelector('details > details > div')).not.toBeVisible(); 191 | }); 192 | 193 | it('returns true to the nested details summary', () => { 194 | expect(subject.container.querySelector('details > details > summary')).toBeVisible(); 195 | }); 196 | 197 | it('returns true to the outer details content', () => { 198 | expect(subject.container.querySelector('details > div')).toBeVisible(); 199 | }); 200 | 201 | it('returns true to the outer details summary', () => { 202 | expect(subject.container.querySelector('details > summary')).toBeVisible(); 203 | }); 204 | }); 205 | 206 | describe("when the outer details isn't opened and the nested one is opened", () => { 207 | beforeEach(() => { 208 | subject = render(` 209 |
210 | Title of visible 211 |
Outer content
212 |
213 | Title of nested details 214 |
Inner content
215 |
216 |
217 | `); 218 | }); 219 | 220 | it('returns false to the nested details content', () => { 221 | expect(subject.container.querySelector('details > details > div')).not.toBeVisible(); 222 | }); 223 | 224 | it('returns false to the nested details summary', () => { 225 | expect(subject.container.querySelector('details > details > summary')).not.toBeVisible(); 226 | }); 227 | 228 | it('returns false to the outer details content', () => { 229 | expect(subject.container.querySelector('details > div')).not.toBeVisible(); 230 | }); 231 | 232 | it('returns true to the outer details summary', () => { 233 | expect(subject.container.querySelector('details > summary')).toBeVisible(); 234 | }); 235 | }); 236 | }); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /src/__tests__/toHaveClassName.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toHaveClassName } from '../toHaveClassName'; 3 | 4 | describe('.toHaveClassName', () => { 5 | const { compare, negativeCompare } = toHaveClassName(); 6 | const { queryByTestId } = render(` 7 |
8 | 11 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | `); 21 | const deleteButton = queryByTestId('delete-button'); 22 | const cancelButton = queryByTestId('cancel-button'); 23 | const svgSpinner = queryByTestId('svg-spinner'); 24 | const oneClass = queryByTestId('only-one-class'); 25 | const noClasses = queryByTestId('no-classes'); 26 | 27 | describe('without exact mode', () => { 28 | it('positive test cases', () => { 29 | expect(deleteButton).toHaveClassName('btn'); 30 | expect(deleteButton).toHaveClassName('btn-danger'); 31 | expect(deleteButton).toHaveClassName('extra'); 32 | expect(deleteButton).not.toHaveClassName('xtra'); 33 | expect(deleteButton).not.toHaveClassName('btn xtra'); 34 | expect(deleteButton).not.toHaveClassName('btn', 'xtra'); 35 | expect(deleteButton).not.toHaveClassName('btn', 'extra xtra'); 36 | expect(deleteButton).toHaveClassName('btn btn-danger'); 37 | expect(deleteButton).toHaveClassName('btn', 'btn-danger'); 38 | expect(deleteButton).toHaveClassName('btn extra', 'btn-danger extra'); 39 | expect(deleteButton).not.toHaveClassName('btn-link'); 40 | expect(cancelButton).not.toHaveClassName('btn-danger'); 41 | expect(svgSpinner).toHaveClassName('spinner'); 42 | expect(svgSpinner).toHaveClassName('clockwise'); 43 | expect(svgSpinner).not.toHaveClassName('wise'); 44 | expect(noClasses).not.toHaveClassName(); 45 | expect(noClasses).not.toHaveClassName(' '); 46 | }); 47 | 48 | it('negative test cases', () => { 49 | const { message: negativeDeleteBtnMessage, pass: negativeDeleteBtnPass } = negativeCompare(deleteButton, 'btn'); 50 | const { message: negativeDeleteBtnDangerMessage, pass: negativeDeleteBtnDangerPass } = negativeCompare( 51 | deleteButton, 52 | 'btn-danger' 53 | ); 54 | const { message: negativeDeleteExtraMessage, pass: negativeDeleteExtraPass } = negativeCompare( 55 | deleteButton, 56 | 'extra' 57 | ); 58 | const { message: positiveDeleteXtraMessage, pass: positiveDeleteXtraPass } = compare(deleteButton, 'xtra'); 59 | const { message: positiveDeleteBtnExtraXtraMessage, pass: positiveDeleteBtnExtraXtraPass } = compare( 60 | deleteButton, 61 | 'btn', 62 | 'extra xtra' 63 | ); 64 | const { message: negativeDeleteBtnBtnDangerMessage, pass: negativeDeleteBtnBtnDangerPass } = negativeCompare( 65 | deleteButton, 66 | 'btn btn-danger' 67 | ); 68 | const { message: negativeDeleteBtnAndBtnDangerMessage, pass: negativeDeleteBtnAndBtnDangerPass } = 69 | negativeCompare(deleteButton, 'btn', 'btn-danger'); 70 | const { message: positiveDeleteBtnLinkMessage, pass: positiveDeleteBtnLinkPass } = compare( 71 | deleteButton, 72 | 'btn-link' 73 | ); 74 | const { message: positiveCancelBtnDangerMessage, pass: positiveCancelBtnDangerPass } = compare( 75 | cancelButton, 76 | 'btn-danger' 77 | ); 78 | const { message: negativeSvgSpinnerMessage, pass: negativeSvgSpinnerPass } = negativeCompare( 79 | svgSpinner, 80 | 'spinner' 81 | ); 82 | const { message: positiveSvgWiseMessage, pass: positiveSvgWisePass } = compare(svgSpinner, 'wise'); 83 | const { message: positiveDeleteNullMessage, pass: positiveDeleteNullPass } = compare(deleteButton); 84 | const { message: positiveDeleteEmptyMessage, pass: positiveDeleteEmptyPass } = compare(deleteButton, ''); 85 | const { message: positiveNoClassesMessage, pass: positiveNoClassesPass } = compare(noClasses); 86 | const { message: negativeDeleteNullMessage, pass: negativeDeleteNullPass } = negativeCompare(deleteButton); 87 | const { message: negativeDeleteWhitespaceMessage, pass: negativeDeleteWhitespacePass } = negativeCompare( 88 | deleteButton, 89 | ' ' 90 | ); 91 | 92 | expect(negativeDeleteBtnPass).toBeFalse(); 93 | expect(negativeDeleteBtnMessage).toMatch(/Expected.*not to have class/); 94 | expect(negativeDeleteBtnDangerPass).toBeFalse(); 95 | expect(negativeDeleteBtnDangerMessage).toMatch(/Expected.*not to have class/); 96 | expect(negativeDeleteExtraPass).toBeFalse(); 97 | expect(negativeDeleteExtraMessage).toMatch(/Expected.*not to have class/); 98 | expect(positiveDeleteXtraPass).toBeFalse(); 99 | expect(positiveDeleteXtraMessage).toMatch(/Expected.*to have class/); 100 | expect(positiveDeleteBtnExtraXtraPass).toBeFalse(); 101 | expect(positiveDeleteBtnExtraXtraMessage).toMatch(/Expected.*to have class/); 102 | expect(negativeDeleteBtnBtnDangerPass).toBeFalse(); 103 | expect(negativeDeleteBtnBtnDangerMessage).toMatch(/Expected.*not to have class/); 104 | expect(negativeDeleteBtnAndBtnDangerPass).toBeFalse(); 105 | expect(negativeDeleteBtnAndBtnDangerMessage).toMatch(/Expected.*not to have class/); 106 | expect(positiveDeleteBtnLinkPass).toBeFalse(); 107 | expect(positiveDeleteBtnLinkMessage).toMatch(/Expected.*to have class/); 108 | expect(positiveCancelBtnDangerPass).toBeFalse(); 109 | expect(positiveCancelBtnDangerMessage).toMatch(/Expected.*to have class/); 110 | expect(negativeSvgSpinnerPass).toBeFalse(); 111 | expect(negativeSvgSpinnerMessage).toMatch(/Expected.*not to have class/); 112 | expect(positiveSvgWisePass).toBeFalse(); 113 | expect(positiveSvgWiseMessage).toMatch(/Expected.*to have class/); 114 | expect(positiveDeleteNullPass).toBeFalse(); 115 | expect(positiveDeleteNullMessage).toMatch(/At least one expected class must be provided/); 116 | expect(positiveDeleteEmptyPass).toBeFalse(); 117 | expect(positiveDeleteEmptyMessage).toMatch(/At least one expected class must be provided/); 118 | expect(positiveNoClassesPass).toBeFalse(); 119 | expect(positiveNoClassesMessage).toMatch(/At least one expected class must be provided/); 120 | expect(negativeDeleteNullPass).toBeFalse(); 121 | expect(negativeDeleteNullMessage).toMatch(/Expected.*not to have classes/); 122 | expect(negativeDeleteWhitespacePass).toBeFalse(); 123 | expect(negativeDeleteWhitespaceMessage).toMatch(/Expected.*not to have classes/); 124 | }); 125 | }); 126 | 127 | describe('with exact mode', () => { 128 | it('positive test cases', () => { 129 | expect(deleteButton).toHaveClassName('btn extra btn-danger', { 130 | exact: true, 131 | }); 132 | expect(deleteButton).not.toHaveClassName('btn extra', { 133 | exact: true, 134 | }); 135 | expect(deleteButton).not.toHaveClassName('btn extra btn-danger foo bar', { 136 | exact: true, 137 | }); 138 | expect(deleteButton).toHaveClassName('btn extra btn-danger', { 139 | exact: false, 140 | }); 141 | expect(deleteButton).toHaveClassName('btn extra', { 142 | exact: false, 143 | }); 144 | expect(deleteButton).not.toHaveClassName('btn extra btn-danger foo bar', { 145 | exact: false, 146 | }); 147 | expect(deleteButton).toHaveClassName('btn', 'extra', 'btn-danger', { 148 | exact: true, 149 | }); 150 | expect(deleteButton).not.toHaveClassName('btn', 'extra', { 151 | exact: true, 152 | }); 153 | expect(deleteButton).not.toHaveClassName('btn', 'extra', 'btn-danger', 'foo', 'bar', { 154 | exact: true, 155 | }); 156 | expect(deleteButton).toHaveClassName('btn', 'extra', 'btn-danger', { 157 | exact: false, 158 | }); 159 | expect(deleteButton).toHaveClassName('btn', 'extra', { 160 | exact: false, 161 | }); 162 | expect(deleteButton).not.toHaveClassName('btn', 'extra', 'btn-danger', 'foo', 'bar', { 163 | exact: false, 164 | }); 165 | expect(oneClass).toHaveClassName('alone', { 166 | exact: true, 167 | }); 168 | expect(oneClass).not.toHaveClassName('alone foo', { 169 | exact: true, 170 | }); 171 | expect(oneClass).not.toHaveClassName('alone', 'foo', { 172 | exact: true, 173 | }); 174 | expect(oneClass).toHaveClassName('alone', { 175 | exact: false, 176 | }); 177 | expect(oneClass).not.toHaveClassName('alone foo', { 178 | exact: false, 179 | }); 180 | expect(oneClass).not.toHaveClassName('alone', 'foo', { 181 | exact: false, 182 | }); 183 | }); 184 | 185 | it('negative test cases', () => { 186 | const { message: negativeAloneExactMessage, pass: negativeAloneExactPass } = negativeCompare(oneClass, 'alone', { 187 | exact: true, 188 | }); 189 | const { message: positiveAloneFooExactMessage, pass: positiveAloneFooExactPass } = compare( 190 | oneClass, 191 | 'alone', 192 | 'foo', 193 | { 194 | exact: true, 195 | } 196 | ); 197 | 198 | expect(negativeAloneExactPass).toBeFalse(); 199 | expect(negativeAloneExactMessage).toMatch(/Expected.*not to have.*EXACTLY.*defined classes/); 200 | expect(positiveAloneFooExactPass).toBeFalse(); 201 | expect(positiveAloneFooExactMessage).toMatch(/Expected.*to have.*EXACTLY.*defined classes/); 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /src/__tests__/toBeChecked.test.js: -------------------------------------------------------------------------------- 1 | import { render } from './helpers/renderer'; 2 | import { toBeChecked } from '../toBeChecked'; 3 | 4 | describe('.toBeChecked', () => { 5 | const { compare, negativeCompare } = toBeChecked(); 6 | it('input type checkbox', () => { 7 | const { queryByTestId } = render(` 8 | 9 | 10 | `); 11 | expect(queryByTestId('checked-checkbox')).toBeChecked(); 12 | expect(queryByTestId('unchecked-checkbox')).not.toBeChecked(); 13 | }); 14 | 15 | it('input type radio', () => { 16 | const { queryByTestId } = render(` 17 | 18 | 19 | `); 20 | expect(queryByTestId('checked-radio')).toBeChecked(); 21 | expect(queryByTestId('unchecked-radio')).not.toBeChecked(); 22 | }); 23 | 24 | it('element w/ checkbox role', () => { 25 | const { queryByTestId } = render(` 26 |
27 |
28 | `); 29 | expect(queryByTestId('aria-checked-checkbox')).toBeChecked(); 30 | expect(queryByTestId('aria-unchecked-checkbox')).not.toBeChecked(); 31 | }); 32 | 33 | it('element w/ radio role', () => { 34 | const { queryByTestId } = render(` 35 |
36 |
37 | `); 38 | expect(queryByTestId('aria-checked-radio')).toBeChecked(); 39 | expect(queryByTestId('aria-unchecked-radio')).not.toBeChecked(); 40 | }); 41 | 42 | it('element w/ switch role', () => { 43 | const { queryByTestId } = render(` 44 |
45 |
46 | `); 47 | expect(queryByTestId('aria-checked-switch')).toBeChecked(); 48 | expect(queryByTestId('aria-unchecked-switch')).not.toBeChecked(); 49 | }); 50 | 51 | it('TO BE ADDED: element w/ menuitemcheckbox role', () => { 52 | const { queryByTestId } = render(` 53 |
54 |
55 | `); 56 | 57 | expect(queryByTestId('checked-aria-menuitemcheckbox')).toBeChecked(); 58 | expect(queryByTestId('unchecked-aria-menuitemcheckbox')).not.toBeChecked(); 59 | }); 60 | 61 | it('throws when input type checkbox is checked but expected not to be', () => { 62 | const { queryByTestId } = render(` 63 | 64 | `); 65 | const { message, pass } = negativeCompare(queryByTestId('checked-input')); 66 | 67 | expect(pass).toBeFalse(); 68 | expect(message).toMatch(/Expected the element.*not to be checked and it.*is checked.*\./); 69 | }); 70 | 71 | it("throws when input type checkbox isn't checked but expected to be", () => { 72 | const { queryByTestId } = render(` 73 | 74 | `); 75 | const { message, pass } = compare(queryByTestId('unchecked-input')); 76 | 77 | expect(pass).toBeFalse(); 78 | expect(message).toMatch(/Expected the element.*to be checked and it.*isn't checked.*\./); 79 | }); 80 | 81 | it('throws when element w/ role checkbox is checked but expected not to be', () => { 82 | const { queryByTestId } = render(` 83 |
84 | `); 85 | const { message, pass } = negativeCompare(queryByTestId('checked-aria-checkbox')); 86 | 87 | expect(pass).toBeFalse(); 88 | expect(message).toMatch(/Expected the element.*not to be checked and it.*is checked.*\./); 89 | }); 90 | 91 | it("throws when element w/ role checkbox isn't checked but expected to be", () => { 92 | const { queryByTestId } = render(` 93 |
94 | `); 95 | const { message, pass } = compare(queryByTestId('checked-aria-checkbox')); 96 | 97 | expect(pass).toBeFalse(); 98 | expect(message).toMatch(/Expected the element.*to be checked and it.*isn't checked.*\./); 99 | }); 100 | 101 | it('throws when input type radio is checked but expected not to be', () => { 102 | const { queryByTestId } = render(` 103 | 104 | `); 105 | const { message, pass } = negativeCompare(queryByTestId('checked-radio-input')); 106 | 107 | expect(pass).toBeFalse(); 108 | expect(message).toMatch(/Expected the element.*not to be checked and it.*is checked.*\./); 109 | }); 110 | 111 | it("throws when input type radio isn't checked but expected to be", () => { 112 | const { queryByTestId } = render(` 113 | 114 | `); 115 | const { message, pass } = compare(queryByTestId('unchecked-radio-input')); 116 | 117 | expect(pass).toBeFalse(); 118 | expect(message).toMatch(/Expected the element.*to be checked and it.*isn't checked.*\./); 119 | }); 120 | 121 | it('throws when element w/ role radio is checked but expected not to be', () => { 122 | const { queryByTestId } = render(` 123 |
124 | `); 125 | const { message, pass } = negativeCompare(queryByTestId('checked-aria-radio')); 126 | 127 | expect(pass).toBeFalse(); 128 | expect(message).toMatch(/Expected the element.*not to be checked and it.*is checked.*\./); 129 | }); 130 | 131 | it("throws when element w/ role radio isn't checked but expected to be", () => { 132 | const { queryByTestId } = render(` 133 |
134 | `); 135 | const { message, pass } = compare(queryByTestId('checked-aria-radio')); 136 | 137 | expect(pass).toBeFalse(); 138 | expect(message).toMatch(/Expected the element.*to be checked and it.*isn't checked.*\./); 139 | }); 140 | 141 | it('throws when element w/ role switch is checked but expected not to be', () => { 142 | const { queryByTestId } = render(` 143 |
144 | `); 145 | const { message, pass } = negativeCompare(queryByTestId('checked-aria-switch')); 146 | 147 | expect(pass).toBeFalse(); 148 | expect(message).toMatch(/Expected the element.*not to be checked and it.*is checked.*\./); 149 | }); 150 | 151 | it("throws when element w/ role switch isn't checked but expected to be", () => { 152 | const { queryByTestId } = render(` 153 |
154 | `); 155 | const { message, pass } = compare(queryByTestId('checked-aria-switch')); 156 | 157 | expect(pass).toBeFalse(); 158 | expect(message).toMatch(/Expected the element.*to be checked and it.*isn't checked.*\./); 159 | }); 160 | 161 | it('throws when element w/ role checkbox has an invalid aria-checked attribute', () => { 162 | const { queryByTestId } = render(` 163 |
164 | `); 165 | const { message, pass } = compare(queryByTestId('invalid-aria-checkbox')); 166 | 167 | expect(pass).toBeFalse(); 168 | expect(message).toMatch( 169 | /Only inputs with type='checkbox\/radio' or elements with .* and a valid aria-checked attribute can be used.*Use.*toHaveValue\(\).*instead/ 170 | ); 171 | }); 172 | 173 | it('throws when element w/ role radio has an invalid aria-checked attribute', () => { 174 | const { queryByTestId } = render(` 175 |
176 | `); 177 | const { message, pass } = compare(queryByTestId('invalid-aria-radio')); 178 | 179 | expect(pass).toBeFalse(); 180 | expect(message).toMatch( 181 | /Only inputs with type='checkbox\/radio' or elements with .* and a valid aria-checked attribute can be used.*Use.*toHaveValue\(\).*instead/ 182 | ); 183 | }); 184 | 185 | it('throws when element w/ role switch has an invalid aria-checked attribute', () => { 186 | const { queryByTestId } = render(` 187 |
188 | `); 189 | const { message, pass } = compare(queryByTestId('invalid-aria-switch')); 190 | 191 | expect(pass).toBeFalse(); 192 | expect(message).toMatch( 193 | /Only inputs with type='checkbox\/radio' or elements with .* and a valid aria-checked attribute can be used.*Use.*toHaveValue\(\).*instead/ 194 | ); 195 | }); 196 | 197 | it('throws when element is not an input regardless of expecting it to be checked or not', () => { 198 | const { queryByTestId } = render(` 199 | 200 | `); 201 | const { message: positiveMessage, pass: positivePass } = compare(queryByTestId('select')); 202 | const { message: negativeMessage, pass: negativePass } = negativeCompare(queryByTestId('select')); 203 | 204 | expect(positivePass).toBeFalse(); 205 | expect(positiveMessage).toMatch( 206 | /Only inputs with type='checkbox\/radio' or elements with .* and a valid aria-checked attribute can be used.*Use.*toHaveValue\(\).*instead/ 207 | ); 208 | 209 | expect(negativePass).toBeFalse(); 210 | expect(negativeMessage).toMatch( 211 | /Only inputs with type='checkbox\/radio' or elements with .* and a valid aria-checked attribute can be used.*Use.*toHaveValue\(\).*instead/ 212 | ); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------