├── .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 |
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 = ``;
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 |
39 | `);
40 |
41 | expect(queryByTestId('valid')).not.toBeInvalid();
42 | expect(invalidFormNode).toBeInvalid();
43 | });
44 |
45 | it('other elements', () => {
46 | const { queryByTestId } = render(`
47 |
48 | -
49 | -
50 | -
51 | -
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 |
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 |

46 |

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 |
14 |
15 |
16 |
17 |
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 |
69 |
74 |
81 |
90 |
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 |
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 |
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 = ``;
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 |
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 | -
80 | -
81 | -
82 | -
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 |
97 |
98 |
99 |
104 |
105 |
106 |
107 |
110 |
113 |
114 |
115 |
116 |
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 |
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 |
128 | Title of visible
129 | Visible details
130 |
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 |
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 |
--------------------------------------------------------------------------------