Skip to content

Commit 09f7f04

Browse files
hiwelognapse
authored andcommitted
feat: Add toBeInvalid and toBeValid matchers (#110)
1 parent d3f5855 commit 09f7f04

File tree

9 files changed

+241
-11
lines changed

9 files changed

+241
-11
lines changed

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ to maintain.
4949
- [`toBeEnabled`](#tobeenabled)
5050
- [`toBeEmpty`](#tobeempty)
5151
- [`toBeInTheDocument`](#tobeinthedocument)
52+
- [`toBeInvalid`](#tobeinvalid)
5253
- [`toBeRequired`](#toberequired)
54+
- [`toBeValid`](#tobevalid)
5355
- [`toBeVisible`](#tobevisible)
5456
- [`toContainElement`](#tocontainelement)
5557
- [`toContainHTML`](#tocontainhtml)
@@ -235,6 +237,45 @@ expect(
235237
236238
<hr />
237239
240+
### `toBeInvalid`
241+
242+
```typescript
243+
toBeInvalid()
244+
```
245+
246+
This allows you to check if an form element is currently invalid.
247+
248+
An element is invalid if it is having an [`aria-invalid` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-invalid_attribute) or if the result of [`checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) are false.
249+
250+
#### Examples
251+
252+
```html
253+
<input data-testid="no-aria-invalid" />
254+
<input data-testid="aria-invalid" aria-invalid />
255+
<input data-testid="aria-invalid-value" aria-invalid="true" />
256+
<input data-testid="aria-invalid-false" aria-invalid="false" />
257+
```
258+
259+
##### Using document.querySelector
260+
261+
```javascript
262+
expect(queryByTestId('no-aria-invalid')).not.toBeInvalid()
263+
expect(queryByTestId('aria-invalid')).toBeInvalid()
264+
expect(queryByTestId('aria-invalid-value')).toBeInvalid()
265+
expect(queryByTestId('aria-invalid-false')).not.toBeInvalid()
266+
```
267+
268+
##### Using dom-testing-library
269+
270+
```javascript
271+
expect(getByTestId(container, 'no-aria-invalid')).not.toBeInvalid()
272+
expect(getByTestId(container, 'aria-invalid')).toBeInvalid()
273+
expect(getByTestId(container, 'aria-invalid-value')).toBeInvalid()
274+
expect(getByTestId(container, 'aria-invalid-false')).not.toBeInvalid()
275+
```
276+
277+
<hr />
278+
238279
### `toBeRequired`
239280
240281
```typescript
@@ -303,6 +344,45 @@ expect(getByTestId(container, 'supported-role-aria')).toBeRequired()
303344
304345
<hr />
305346
347+
### `toBeValid`
348+
349+
```typescript
350+
toBeValid()
351+
```
352+
353+
This allows you to check if the value of a form element is currently valid.
354+
355+
An element is valid if it is not having an [`aria-invalid` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-invalid_attribute) or having `false` as a value and returning `true` when calling [`checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation).
356+
357+
#### Examples
358+
359+
```html
360+
<input data-testid="no-aria-invalid" />
361+
<input data-testid="aria-invalid" aria-invalid />
362+
<input data-testid="aria-invalid-value" aria-invalid="true" />
363+
<input data-testid="aria-invalid-false" aria-invalid="false" />
364+
```
365+
366+
##### Using document.querySelector
367+
368+
```javascript
369+
expect(queryByTestId('no-aria-invalid')).toBeValid()
370+
expect(queryByTestId('aria-invalid')).not.toBeValid()
371+
expect(queryByTestId('aria-invalid-value')).not.toBeValid()
372+
expect(queryByTestId('aria-invalid-false')).toBeValid()
373+
```
374+
375+
##### Using dom-testing-library
376+
377+
```javascript
378+
expect(getByTestId(container, 'no-aria-invalid')).toBeValid()
379+
expect(getByTestId(container, 'aria-invalid')).not.toBeValid()
380+
expect(getByTestId(container, 'aria-invalid-value')).not.toBeValid()
381+
expect(getByTestId(container, 'aria-invalid-false')).toBeValid()
382+
```
383+
384+
<hr />
385+
306386
### `toBeVisible`
307387
308388
```typescript

extend-expect.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ declare namespace jest {
99
toBeEmpty(): R
1010
toBeDisabled(): R
1111
toBeEnabled(): R
12+
toBeInvalid(): R
1213
toBeRequired(): R
14+
toBeValid(): R
1315
toContainElement(element: HTMLElement | SVGElement | null): R
1416
toContainHTML(htmlText: string): R
1517
toHaveAttribute(attr: string, value?: any): R

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"redent": "^2.0.0"
4444
},
4545
"devDependencies": {
46-
"jsdom": "^12.2.0",
46+
"jsdom": "^15.1.0",
4747
"kcd-scripts": "^0.44.0"
4848
},
4949
"eslintConfig": {

src/__tests__/to-be-invalid.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {JSDOM} from 'jsdom'
2+
import {render} from './helpers/test-utils'
3+
4+
/*
5+
* This function is being used to test if `.toBeInvalid` and `.toBeValid`
6+
* are correctly triggered by the DOM Node method `.checkValidity()`, part
7+
* of the Web API.
8+
*
9+
* For this check, we are using the `jsdom` library to return a DOM Node
10+
* sending the good information to our test.
11+
*
12+
* We are using this library because without it `.checkValidity()` returns
13+
* always `true` when using `yarn test` in a terminal.
14+
*
15+
* Please consult the PR 110 to get more information:
16+
* https://github.com/testing-library/jest-dom/pull/110
17+
*
18+
* @link https://github.com/testing-library/jest-dom/pull/110
19+
* @link https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation
20+
* @link https://github.com/jsdom/jsdom
21+
*/
22+
function getDOMInput(htmlString) {
23+
const dom = new JSDOM(htmlString)
24+
25+
return dom.window.document.querySelector('input')
26+
}
27+
28+
const invalidInputNode = getDOMInput(
29+
`<input pattern="[a-z]{1,3}" value="test+">`,
30+
)
31+
32+
test('.toBeInvalid', () => {
33+
const {queryByTestId} = render(`
34+
<div>
35+
<input data-testid="no-aria-invalid">
36+
<input data-testid="aria-invalid" aria-invalid>
37+
<input data-testid="aria-invalid-value" aria-invalid="true">
38+
<input data-testid="aria-invalid-false" aria-invalid="false">
39+
</div>
40+
`)
41+
42+
expect(queryByTestId('no-aria-invalid')).not.toBeInvalid()
43+
expect(queryByTestId('aria-invalid')).toBeInvalid()
44+
expect(queryByTestId('aria-invalid-value')).toBeInvalid()
45+
expect(queryByTestId('aria-invalid-false')).not.toBeInvalid()
46+
expect(invalidInputNode).toBeInvalid()
47+
48+
// negative test cases wrapped in throwError assertions for coverage.
49+
expect(() =>
50+
expect(queryByTestId('no-aria-invalid')).toBeInvalid(),
51+
).toThrowError()
52+
expect(() =>
53+
expect(queryByTestId('aria-invalid')).not.toBeInvalid(),
54+
).toThrowError()
55+
expect(() =>
56+
expect(queryByTestId('aria-invalid-value')).not.toBeInvalid(),
57+
).toThrowError()
58+
expect(() =>
59+
expect(queryByTestId('aria-invalid-false')).toBeInvalid(),
60+
).toThrowError()
61+
expect(() => expect(invalidInputNode).not.toBeInvalid()).toThrowError()
62+
})
63+
64+
test('.toBeValid', () => {
65+
const {queryByTestId} = render(`
66+
<div>
67+
<input data-testid="no-aria-invalid">
68+
<input data-testid="aria-invalid" aria-invalid>
69+
<input data-testid="aria-invalid-value" aria-invalid="true">
70+
<input data-testid="aria-invalid-false" aria-invalid="false">
71+
</div>
72+
`)
73+
74+
expect(queryByTestId('no-aria-invalid')).toBeValid()
75+
expect(queryByTestId('aria-invalid')).not.toBeValid()
76+
expect(queryByTestId('aria-invalid-value')).not.toBeValid()
77+
expect(queryByTestId('aria-invalid-false')).toBeValid()
78+
expect(invalidInputNode).not.toBeValid()
79+
80+
// negative test cases wrapped in throwError assertions for coverage.
81+
expect(() =>
82+
expect(queryByTestId('no-aria-invalid')).not.toBeValid(),
83+
).toThrowError()
84+
expect(() => expect(queryByTestId('aria-invalid')).toBeValid()).toThrowError()
85+
expect(() =>
86+
expect(queryByTestId('aria-invalid-value')).toBeValid(),
87+
).toThrowError()
88+
expect(() =>
89+
expect(queryByTestId('aria-invalid-false')).not.toBeValid(),
90+
).toThrowError()
91+
expect(() => expect(invalidInputNode).toBeValid()).toThrowError()
92+
})

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {toHaveFormValues} from './to-have-form-values'
1212
import {toBeVisible} from './to-be-visible'
1313
import {toBeDisabled, toBeEnabled} from './to-be-disabled'
1414
import {toBeRequired} from './to-be-required'
15+
import {toBeInvalid, toBeValid} from './to-be-invalid'
1516

1617
export {
1718
toBeInTheDOM,
@@ -29,4 +30,6 @@ export {
2930
toBeDisabled,
3031
toBeEnabled,
3132
toBeRequired,
33+
toBeInvalid,
34+
toBeValid,
3235
}

src/to-be-disabled.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {matcherHint, printReceived} from 'jest-matcher-utils'
2-
import {checkHtmlElement} from './utils'
2+
import {checkHtmlElement, getTag} from './utils'
33

44
// form elements that support 'disabled'
55
const FORM_TAGS = [
@@ -12,10 +12,6 @@ const FORM_TAGS = [
1212
'textarea',
1313
]
1414

15-
function getTag(element) {
16-
return element.tagName && element.tagName.toLowerCase()
17-
}
18-
1915
/*
2016
* According to specification:
2117
* If <fieldset> is disabled, the form controls that are its descendants,

src/to-be-invalid.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {matcherHint, printReceived} from 'jest-matcher-utils'
2+
import {checkHtmlElement, getTag} from './utils'
3+
4+
const FORM_TAGS = ['input', 'select', 'textarea']
5+
6+
function isElementHavingAriaInvalid(element) {
7+
return (
8+
element.hasAttribute('aria-invalid') &&
9+
element.getAttribute('aria-invalid') !== 'false'
10+
)
11+
}
12+
13+
function isElementInvalid(element) {
14+
return !element.checkValidity()
15+
}
16+
17+
export function toBeInvalid(element) {
18+
checkHtmlElement(element, toBeInvalid, this)
19+
20+
const isInvalid =
21+
isElementHavingAriaInvalid(element) || isElementInvalid(element)
22+
23+
return {
24+
pass: isInvalid,
25+
message: () => {
26+
const is = isInvalid ? 'is' : 'is not'
27+
return [
28+
matcherHint(`${this.isNot ? '.not' : ''}.toBeInvalid`, 'element', ''),
29+
'',
30+
`Received element ${is} currently invalid:`,
31+
` ${printReceived(element.cloneNode(false))}`,
32+
].join('\n')
33+
},
34+
}
35+
}
36+
37+
export function toBeValid(element) {
38+
checkHtmlElement(element, toBeValid, this)
39+
40+
const isValid =
41+
!isElementHavingAriaInvalid(element) &&
42+
(FORM_TAGS.includes(getTag(element)) && !isElementInvalid(element))
43+
44+
return {
45+
pass: isValid,
46+
message: () => {
47+
const is = isValid ? 'is' : 'is not'
48+
return [
49+
matcherHint(`${this.isNot ? '.not' : ''}.toBeValid`, 'element', ''),
50+
'',
51+
`Received element ${is} currently valid:`,
52+
` ${printReceived(element.cloneNode(false))}`,
53+
].join('\n')
54+
},
55+
}
56+
}

src/to-be-required.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {matcherHint, printReceived} from 'jest-matcher-utils'
2-
import {checkHtmlElement} from './utils'
2+
import {checkHtmlElement, getTag} from './utils'
33

44
// form elements that support 'required'
55
const FORM_TAGS = ['select', 'textarea']
@@ -23,10 +23,6 @@ const SUPPORTED_ARIA_ROLES = [
2323
'tree',
2424
]
2525

26-
function getTag(element) {
27-
return element.tagName && element.tagName.toLowerCase()
28-
}
29-
3026
function isRequiredOnFormTagsExceptInput(element) {
3127
return FORM_TAGS.includes(getTag(element)) && element.hasAttribute('required')
3228
}

src/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ function normalize(text) {
133133
return text.replace(/\s+/g, ' ').trim()
134134
}
135135

136+
function getTag(element) {
137+
return element.tagName && element.tagName.toLowerCase()
138+
}
139+
136140
export {
137141
HtmlElementTypeError,
138142
checkHtmlElement,
@@ -141,4 +145,5 @@ export {
141145
getMessage,
142146
matches,
143147
normalize,
148+
getTag,
144149
}

0 commit comments

Comments
 (0)