Skip to content

Commit ed0b272

Browse files
connormeredithgnapse
authored andcommitted
feat: Add toBeChecked matcher (#141)
* feat: Add toBeChecked matcher * Added support for type=radio, role=checkbox and role=radio elements to .toBeChecked() matcher.
1 parent d048768 commit ed0b272

File tree

6 files changed

+311
-7
lines changed

6 files changed

+311
-7
lines changed

README.md

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ clear to read and to maintain.
6666
- [`toHaveStyle`](#tohavestyle)
6767
- [`toHaveTextContent`](#tohavetextcontent)
6868
- [`toHaveValue`](#tohavevalue)
69+
- [`toBeChecked`](#tobechecked)
6970
- [Deprecated matchers](#deprecated-matchers)
7071
- [`toBeInTheDOM`](#tobeinthedom)
7172
- [Inspiration](#inspiration)
@@ -97,7 +98,8 @@ Import `@testing-library/jest-dom/extend-expect` once (for instance in your
9798
import '@testing-library/jest-dom/extend-expect'
9899
```
99100

100-
> Note: If you're using TypeScript, make sure your setup file is a `.ts` and not a `.js` to include the necessary types.
101+
> Note: If you're using TypeScript, make sure your setup file is a `.ts` and not
102+
> a `.js` to include the necessary types.
101103
102104
Alternatively, you can selectively import only the matchers you intend to use,
103105
and extend jest's `expect` yourself:
@@ -108,7 +110,9 @@ import {toBeInTheDocument, toHaveClass} from '@testing-library/jest-dom'
108110
expect.extend({toBeInTheDocument, toHaveClass})
109111
```
110112

111-
> Note: when using TypeScript, this way of importing matchers won't provide the necessary type definitions. More on this [here](https://github.com/testing-library/jest-dom/pull/11#issuecomment-387817459).
113+
> Note: when using TypeScript, this way of importing matchers won't provide the
114+
> necessary type definitions. More on this
115+
> [here](https://github.com/testing-library/jest-dom/pull/11#issuecomment-387817459).
112116
113117
## Custom matchers
114118

@@ -919,7 +923,8 @@ toHaveValue(value: string | string[] | number)
919923
This allows you to check whether the given form element has the specified value.
920924
It accepts `<input>`, `<select>` and `<textarea>` elements with the exception of
921925
of `<input type="checkbox">` and `<input type="radio">`, which can be
922-
meaningfully matched only using [`toHaveFormValue`](#tohaveformvalues).
926+
meaningfully matched only using [`toBeChecked`](#tobechecked) or
927+
[`toHaveFormValue`](#tohaveformvalues).
923928
924929
For all other form elements, the value is matched using the same algorithm as in
925930
[`toHaveFormValue`](#tohaveformvalues) does.
@@ -967,6 +972,98 @@ expect(emptyInput).not.toHaveValue()
967972
expect(selectInput).not.toHaveValue(['second', 'third'])
968973
```
969974
975+
<hr />
976+
977+
### `toBeChecked`
978+
979+
```typescript
980+
toBeChecked()
981+
```
982+
983+
This allows you to check whether the given element is checked. It accepts an
984+
`input` of type `checkbox` or `radio` and elements with a `role` of `checkbox`
985+
or `radio` with a valid `aria-checked` attribute of `"true"` or `"false"`.
986+
987+
#### Examples
988+
989+
```html
990+
<input type="checkbox" checked data-testid="input-checkbox-checked" />
991+
<input type="checkbox" data-testid="input-checkbox-unchecked" />
992+
<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />
993+
<div
994+
role="checkbox"
995+
aria-checked="false"
996+
data-testid="aria-checkbox-unchecked"
997+
/>
998+
999+
<input type="radio" checked value="foo" data-testid="input-radio-checked" />
1000+
<input type="radio" value="foo" data-testid="input-radio-unchecked" />
1001+
<div role="radio" aria-checked="true" data-testid="aria-radio-checked" />
1002+
<div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" />
1003+
```
1004+
1005+
##### Using document.querySelector
1006+
1007+
```javascript
1008+
const inputCheckboxChecked = document.querySelector(
1009+
'[data-testid="input-checkbox-checked"]',
1010+
)
1011+
const inputCheckboxUnchecked = document.querySelector(
1012+
'[data-testid="input-checkbox-unchecked"]',
1013+
)
1014+
const ariaCheckboxChecked = document.querySelector(
1015+
'[data-testid="aria-checkbox-checked"]',
1016+
)
1017+
const ariaCheckboxUnchecked = document.querySelector(
1018+
'[data-testid="aria-checkbox-unchecked"]',
1019+
)
1020+
expect(inputCheckboxChecked).toBeChecked()
1021+
expect(inputCheckboxUnchecked).not.toBeChecked()
1022+
expect(ariaCheckboxChecked).toBeChecked()
1023+
expect(ariaCheckboxUnchecked).not.toBeChecked()
1024+
1025+
const inputRadioChecked = document.querySelector(
1026+
'[data-testid="input-radio-checked"]',
1027+
)
1028+
const inputRadioUnchecked = document.querySelector(
1029+
'[data-testid="input-radio-unchecked"]',
1030+
)
1031+
const ariaRadioChecked = document.querySelector(
1032+
'[data-testid="aria-radio-checked"]',
1033+
)
1034+
const ariaRadioUnchecked = document.querySelector(
1035+
'[data-testid="aria-radio-unchecked"]',
1036+
)
1037+
expect(inputRadioChecked).toBeChecked()
1038+
expect(inputRadioUnchecked).not.toBeChecked()
1039+
expect(ariaRadioChecked).toBeChecked()
1040+
expect(ariaRadioUnchecked).not.toBeChecked()
1041+
```
1042+
1043+
##### Using DOM Testing Library
1044+
1045+
```javascript
1046+
const {getByTestId} = render(/* Rendered HTML */)
1047+
1048+
const inputCheckboxChecked = getByTestId('input-checkbox-checked')
1049+
const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked')
1050+
const ariaCheckboxChecked = getByTestId('aria-checkbox-checked')
1051+
const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked')
1052+
expect(inputCheckboxChecked).toBeChecked()
1053+
expect(inputCheckboxUnchecked).not.toBeChecked()
1054+
expect(ariaCheckboxChecked).toBeChecked()
1055+
expect(ariaCheckboxUnchecked).not.toBeChecked()
1056+
1057+
const inputRadioChecked = getByTestId('input-radio-checked')
1058+
const inputRadioUnchecked = getByTestId('input-radio-unchecked')
1059+
const ariaRadioChecked = getByTestId('aria-radio-checked')
1060+
const ariaRadioUnchecked = getByTestId('aria-radio-unchecked')
1061+
expect(inputRadioChecked).toBeChecked()
1062+
expect(inputRadioUnchecked).not.toBeChecked()
1063+
expect(ariaRadioChecked).toBeChecked()
1064+
expect(ariaRadioUnchecked).not.toBeChecked()
1065+
```
1066+
9701067
## Deprecated matchers
9711068
9721069
### `toBeInTheDOM`
@@ -1003,8 +1100,9 @@ expect(document.querySelector('.cancel-button')).toBeTruthy()
10031100
10041101
## Inspiration
10051102
1006-
This whole library was extracted out of Kent C. Dodds' [DOM Testing Library][dom-testing-library],
1007-
which was in turn extracted out of [React Testing Library][react-testing-library].
1103+
This whole library was extracted out of Kent C. Dodds' [DOM Testing
1104+
Library][dom-testing-library], which was in turn extracted out of [React Testing
1105+
Library][react-testing-library].
10081106
10091107
The intention is to make this available to be used independently of these other
10101108
libraries, and also to make it more clear that these other libraries are
@@ -1021,7 +1119,8 @@ here!
10211119
> confidence they can give you.][guiding-principle]
10221120
10231121
This library follows the same guiding principles as its mother library [DOM
1024-
Testing Library][dom-testing-library]. Go [check them out][guiding-principle] for more details.
1122+
Testing Library][dom-testing-library]. Go [check them out][guiding-principle]
1123+
for more details.
10251124
10261125
Additionally, with respect to custom DOM matchers, this library aims to maintain
10271126
a minimal but useful set of them, while avoiding bloating itself with merely

extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ declare namespace jest {
2424
options?: {normalizeWhitespace: boolean},
2525
): R
2626
toHaveValue(value?: string | string[] | number): R
27+
toBeChecked(): R
2728
}
2829
}

src/__tests__/to-be-checked.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {render} from './helpers/test-utils'
2+
3+
describe('.toBeChecked', () => {
4+
test('handles checkbox input', () => {
5+
const {queryByTestId} = render(`
6+
<input type="checkbox" checked data-testid="input-checkbox-checked" />
7+
<input type="checkbox" data-testid="input-checkbox-unchecked" />
8+
`)
9+
10+
expect(queryByTestId('input-checkbox-checked')).toBeChecked()
11+
expect(queryByTestId('input-checkbox-unchecked')).not.toBeChecked()
12+
})
13+
14+
test('handles radio input', () => {
15+
const {queryByTestId} = render(`
16+
<input type="radio" checked value="foo" data-testid="input-radio-checked" />
17+
<input type="radio" value="foo" data-testid="input-radio-unchecked" />
18+
`)
19+
20+
expect(queryByTestId('input-radio-checked')).toBeChecked()
21+
expect(queryByTestId('input-radio-unchecked')).not.toBeChecked()
22+
})
23+
24+
test('handles element with role="checkbox"', () => {
25+
const {queryByTestId} = render(`
26+
<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />
27+
<div role="checkbox" aria-checked="false" data-testid="aria-checkbox-unchecked" />
28+
`)
29+
30+
expect(queryByTestId('aria-checkbox-checked')).toBeChecked()
31+
expect(queryByTestId('aria-checkbox-unchecked')).not.toBeChecked()
32+
})
33+
34+
test('handles element with role="radio"', () => {
35+
const {queryByTestId} = render(`
36+
<div role="radio" aria-checked="true" data-testid="aria-radio-checked" />
37+
<div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" />
38+
`)
39+
40+
expect(queryByTestId('aria-radio-checked')).toBeChecked()
41+
expect(queryByTestId('aria-radio-unchecked')).not.toBeChecked()
42+
})
43+
44+
test('throws when checkbox input is checked but expected not to be', () => {
45+
const {queryByTestId} = render(
46+
`<input type="checkbox" checked data-testid="input-checked" />`,
47+
)
48+
49+
expect(() =>
50+
expect(queryByTestId('input-checked')).not.toBeChecked(),
51+
).toThrowError()
52+
})
53+
54+
test('throws when input checkbox is not checked but expected to be', () => {
55+
const {queryByTestId} = render(
56+
`<input type="checkbox" data-testid="input-empty" />`,
57+
)
58+
59+
expect(() =>
60+
expect(queryByTestId('input-empty')).toBeChecked(),
61+
).toThrowError()
62+
})
63+
64+
test('throws when element with role="checkbox" is checked but expected not to be', () => {
65+
const {queryByTestId} = render(
66+
`<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />`,
67+
)
68+
69+
expect(() =>
70+
expect(queryByTestId('aria-checkbox-checked')).not.toBeChecked(),
71+
).toThrowError()
72+
})
73+
74+
test('throws when element with role="checkbox" is not checked but expected to be', () => {
75+
const {queryByTestId} = render(
76+
`<div role="checkbox" aria-checked="false" data-testid="aria-checkbox-unchecked" />`,
77+
)
78+
79+
expect(() =>
80+
expect(queryByTestId('aria-checkbox-unchecked')).toBeChecked(),
81+
).toThrowError()
82+
})
83+
84+
test('throws when radio input is checked but expected not to be', () => {
85+
const {queryByTestId} = render(
86+
`<input type="radio" checked data-testid="input-radio-checked" />`,
87+
)
88+
89+
expect(() =>
90+
expect(queryByTestId('input-radio-checked')).not.toBeChecked(),
91+
).toThrowError()
92+
})
93+
94+
test('throws when input radio is not checked but expected to be', () => {
95+
const {queryByTestId} = render(
96+
`<input type="radio" data-testid="input-radio-unchecked" />`,
97+
)
98+
99+
expect(() =>
100+
expect(queryByTestId('input-radio-unchecked')).toBeChecked(),
101+
).toThrowError()
102+
})
103+
104+
test('throws when element with role="radio" is checked but expected not to be', () => {
105+
const {queryByTestId} = render(
106+
`<div role="radio" aria-checked="true" data-testid="aria-radio-checked" />`,
107+
)
108+
109+
expect(() =>
110+
expect(queryByTestId('aria-radio-checked')).not.toBeChecked(),
111+
).toThrowError()
112+
})
113+
114+
test('throws when element with role="radio" is not checked but expected to be', () => {
115+
const {queryByTestId} = render(
116+
`<div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" />`,
117+
)
118+
119+
expect(() =>
120+
expect(queryByTestId('aria-checkbox-unchecked')).toBeChecked(),
121+
).toThrowError()
122+
})
123+
124+
test('throws when element with role="checkbox" has an invalid aria-checked attribute', () => {
125+
const {queryByTestId} = render(
126+
`<div role="checkbox" aria-checked="something" data-testid="aria-checkbox-invalid" />`,
127+
)
128+
129+
expect(() =>
130+
expect(queryByTestId('aria-checkbox-invalid')).toBeChecked(),
131+
).toThrowError(
132+
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox" or role="radio" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
133+
)
134+
})
135+
136+
test('throws when element with role="radio" has an invalid aria-checked attribute', () => {
137+
const {queryByTestId} = render(
138+
`<div role="radio" aria-checked="something" data-testid="aria-radio-invalid" />`,
139+
)
140+
141+
expect(() =>
142+
expect(queryByTestId('aria-radio-invalid')).toBeChecked(),
143+
).toThrowError(
144+
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox" or role="radio" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
145+
)
146+
})
147+
148+
test('throws when the element is not an input', () => {
149+
const {queryByTestId} = render(`<select data-testid="select"></select>`)
150+
expect(() => expect(queryByTestId('select')).toBeChecked()).toThrowError(
151+
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox" or role="radio" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
152+
)
153+
})
154+
})
155+
156+
/* eslint max-lines-per-function:0 */

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {toBeDisabled, toBeEnabled} from './to-be-disabled'
1414
import {toBeRequired} from './to-be-required'
1515
import {toBeInvalid, toBeValid} from './to-be-invalid'
1616
import {toHaveValue} from './to-have-value'
17+
import {toBeChecked} from './to-be-checked'
1718

1819
export {
1920
toBeInTheDOM,
@@ -34,4 +35,5 @@ export {
3435
toBeInvalid,
3536
toBeValid,
3637
toHaveValue,
38+
toBeChecked,
3739
}

src/to-be-checked.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {matcherHint, printReceived} from 'jest-matcher-utils'
2+
import {checkHtmlElement} from './utils'
3+
4+
export function toBeChecked(element) {
5+
checkHtmlElement(element, toBeChecked, this)
6+
7+
const isValidInput = () => {
8+
return (
9+
element.tagName.toLowerCase() === 'input' &&
10+
['checkbox', 'radio'].includes(element.type)
11+
)
12+
}
13+
14+
const isValidAriaElement = () => {
15+
return (
16+
['checkbox', 'radio'].includes(element.getAttribute('role')) &&
17+
['true', 'false'].includes(element.getAttribute('aria-checked'))
18+
)
19+
}
20+
21+
if (!isValidInput() && !isValidAriaElement()) {
22+
return {
23+
pass: false,
24+
message: () =>
25+
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox" or role="radio" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
26+
}
27+
}
28+
29+
const isChecked = () => {
30+
if (isValidInput()) return element.checked
31+
return element.getAttribute('aria-checked') === 'true'
32+
}
33+
34+
return {
35+
pass: isChecked(),
36+
message: () => {
37+
const is = isChecked() ? 'is' : 'is not'
38+
return [
39+
matcherHint(`${this.isNot ? '.not' : ''}.toBeChecked`, 'element', ''),
40+
'',
41+
`Received element ${is} checked:`,
42+
` ${printReceived(element.cloneNode(false))}`,
43+
].join('\n')
44+
},
45+
}
46+
}

src/to-have-value.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function toHaveValue(htmlElement, expectedValue) {
1515
['checkbox', 'radio'].includes(htmlElement.type)
1616
) {
1717
throw new Error(
18-
'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toHaveFormValues() instead',
18+
'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toBeChecked() for type=checkbox or .toHaveFormValues() instead',
1919
)
2020
}
2121

0 commit comments

Comments
 (0)