Skip to content

Commit d3f5855

Browse files
hiwelognapse
authored andcommitted
feat: Add toBeRequired matcher (#109)
1 parent b2b4bd3 commit d3f5855

File tree

6 files changed

+221
-2
lines changed

6 files changed

+221
-2
lines changed

.all-contributorsrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,17 @@
247247
"contributions": [
248248
"code"
249249
]
250+
},
251+
{
252+
"login": "hiwelo",
253+
"name": "hiwelo.",
254+
"avatar_url": "https://avatars0.githubusercontent.com/u/4989733?v=4",
255+
"profile": "https://raccoon.studio",
256+
"contributions": [
257+
"code",
258+
"ideas",
259+
"test"
260+
]
250261
}
251262
]
252263
}

README.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
[![downloads][downloads-badge]][npmtrends]
1717
[![MIT License][license-badge]][license]
1818

19-
[![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors)
19+
[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat-square)](#contributors)
2020
[![PRs Welcome][prs-badge]][prs]
2121
[![Code of Conduct][coc-badge]][coc]
2222

@@ -49,6 +49,7 @@ to maintain.
4949
- [`toBeEnabled`](#tobeenabled)
5050
- [`toBeEmpty`](#tobeempty)
5151
- [`toBeInTheDocument`](#tobeinthedocument)
52+
- [`toBeRequired`](#toberequired)
5253
- [`toBeVisible`](#tobevisible)
5354
- [`toContainElement`](#tocontainelement)
5455
- [`toContainHTML`](#tocontainhtml)
@@ -234,6 +235,74 @@ expect(
234235
235236
<hr />
236237
238+
### `toBeRequired`
239+
240+
```typescript
241+
toBeRequired()
242+
```
243+
244+
This allows you to check if an form element is currently required.
245+
246+
An element is required if it is having a `required` or `aria-required="true"` attribute.
247+
248+
#### Examples
249+
250+
```html
251+
<input data-testid="required-input" required />
252+
<input data-testid="aria-required-input" aria-required="true" />
253+
<input data-testid="conflicted-input" required aria-required="false" />
254+
<input data-testid="aria-not-required-input" aria-required="false" />
255+
<input data-testid="optional-input" />
256+
<input data-testid="unsupported-type" type="image" required />
257+
<select data-testid="select" required></select>
258+
<textarea data-testid="textarea" required></textarea>
259+
<div data-testid="supported-role" role="tree" required></div>
260+
<div data-testid="supported-role-aria" role="tree" aria-required="true"></div>
261+
```
262+
263+
##### Using document.querySelector
264+
265+
```javascript
266+
expect(document.querySelector('[data-testid="required-input"]')).toBeRequired()
267+
expect(
268+
document.querySelector('[data-testid="aria-required-input"]'),
269+
).toBeRequired()
270+
expect(
271+
document.querySelector('[data-testid="conflicted-input"]'),
272+
).toBeRequired()
273+
expect(
274+
document.querySelector('[data-testid="aria-not-required-input"]'),
275+
).not.toBeRequired()
276+
expect(
277+
document.querySelector('[data-testid="unsupported-type"]'),
278+
).not.toBeRequired()
279+
expect(document.querySelector('[data-testid="select"]')).toBeRequired()
280+
expect(document.querySelector('[data-testid="textarea"]')).toBeRequired()
281+
expect(
282+
document.querySelector('[data-testid="supported-role"]'),
283+
).not.toBeRequired()
284+
expect(
285+
document.querySelector('[data-testid="supported-role-aria"]'),
286+
).toBeRequired()
287+
```
288+
289+
##### Using dom-testing-library
290+
291+
```javascript
292+
expect(getByTestId(container, 'required-input')).toBeRequired()
293+
expect(getByTestId(container, 'aria-required-input')).toBeRequired()
294+
expect(getByTestId(container, 'conflicted-input')).toBeRequired()
295+
expect(getByTestId(container, 'aria-not-required-input')).not.toBeRequired()
296+
expect(getByTestId(container, 'optional-input')).not.toBeRequired()
297+
expect(getByTestId(container, 'unsupported-type')).not.toBeRequired()
298+
expect(getByTestId(container, 'select')).toBeRequired()
299+
expect(getByTestId(container, 'textarea')).toBeRequired()
300+
expect(getByTestId(container, 'supported-role')).not.toBeRequired()
301+
expect(getByTestId(container, 'supported-role-aria')).toBeRequired()
302+
```
303+
304+
<hr />
305+
237306
### `toBeVisible`
238307
239308
```typescript
@@ -763,7 +832,8 @@ Thanks goes to these people ([emoji key][emojis]):
763832
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
764833
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;" alt="Anto Aravinth"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Tests") [📖](https://github.com/testing-library/jest-dom/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;" alt="Jonah Moses"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;" alt="Łukasz Gandecki"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Tests") [📖](https://github.com/testing-library/jest-dom/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;" alt="Ivan Babak"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/4439618?v=4" width="100px;" alt="Jesse Day"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;" alt="Ernesto García"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=gnapse "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=gnapse "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=gnapse "Tests") | [<img src="https://avatars0.githubusercontent.com/u/79312?v=4" width="100px;" alt="Mark Volkmann"/><br /><sub><b>Mark Volkmann</b></sub>](http://ociweb.com/mark/)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Amvolkmann "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=mvolkmann "Code") |
765834
| [<img src="https://avatars1.githubusercontent.com/u/1659099?v=4" width="100px;" alt="smacpherson64"/><br /><sub><b>smacpherson64</b></sub>](https://github.com/smacpherson64)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=smacpherson64 "Tests") | [<img src="https://avatars2.githubusercontent.com/u/132233?v=4" width="100px;" alt="John Gozde"/><br /><sub><b>John Gozde</b></sub>](https://github.com/jgoz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Ajgoz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=jgoz "Code") | [<img src="https://avatars2.githubusercontent.com/u/7830590?v=4" width="100px;" alt="Iwona"/><br /><sub><b>Iwona</b></sub>](https://github.com/callada)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=callada "Code") [📖](https://github.com/testing-library/jest-dom/commits?author=callada "Documentation") [⚠️](https://github.com/testing-library/jest-dom/commits?author=callada "Tests") | [<img src="https://avatars0.githubusercontent.com/u/840609?v=4" width="100px;" alt="Lewis"/><br /><sub><b>Lewis</b></sub>](https://github.com/6ewis)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=6ewis "Code") | [<img src="https://avatars3.githubusercontent.com/u/2339362?v=4" width="100px;" alt="Leandro Lourenci"/><br /><sub><b>Leandro Lourenci</b></sub>](https://blog.lourenci.com/)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Alourenci "Bug reports") [📖](https://github.com/testing-library/jest-dom/commits?author=lourenci "Documentation") [💻](https://github.com/testing-library/jest-dom/commits?author=lourenci "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=lourenci "Tests") | [<img src="https://avatars1.githubusercontent.com/u/626420?v=4" width="100px;" alt="Shukhrat Mukimov"/><br /><sub><b>Shukhrat Mukimov</b></sub>](https://github.com/mufasa71)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Amufasa71 "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/1481264?v=4" width="100px;" alt="Roman Usherenko"/><br /><sub><b>Roman Usherenko</b></sub>](https://github.com/dreyks)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=dreyks "Code") [⚠️](https://github.com/testing-library/jest-dom/commits?author=dreyks "Tests") |
766-
| [<img src="https://avatars1.githubusercontent.com/u/648?v=4" width="100px;" alt="Joe Hsu"/><br /><sub><b>Joe Hsu</b></sub>](http://josephhsu.com)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=jhsu "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/3068563?v=4" width="100px;" alt="Haz"/><br /><sub><b>Haz</b></sub>](https://twitter.com/diegohaz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Adiegohaz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=diegohaz "Code") | [<img src="https://avatars3.githubusercontent.com/u/463904?v=4" width="100px;" alt="Revath S Kumar"/><br /><sub><b>Revath S Kumar</b></sub>](https://blog.revathskumar.com)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=revathskumar "Code") |
835+
| [<img src="https://avatars1.githubusercontent.com/u/648?v=4" width="100px;" alt="Joe Hsu"/><br /><sub><b>Joe Hsu</b></sub>](http://josephhsu.com)<br />[📖](https://github.com/testing-library/jest-dom/commits?author=jhsu "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/3068563?v=4" width="100px;" alt="Haz"/><br /><sub><b>Haz</b></sub>](https://twitter.com/diegohaz)<br />[🐛](https://github.com/testing-library/jest-dom/issues?q=author%3Adiegohaz "Bug reports") [💻](https://github.com/testing-library/jest-dom/commits?author=diegohaz "Code") | [<img src="https://avatars3.githubusercontent.com/u/463904?v=4" width="100px;" alt="Revath S Kumar"/><br /><sub><b>Revath S Kumar</b></sub>](https://blog.revathskumar.com)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=revathskumar "Code") | [<img src="https://avatars0.githubusercontent.com/u/4989733?v=4" width="100px;" alt="hiwelo."/><br /><sub><b>hiwelo.</b></sub>](https://raccoon.studio)<br />[💻](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Code") [🤔](#ideas-hiwelo "Ideas, Planning, & Feedback") [⚠️](https://github.com/testing-library/jest-dom/commits?author=hiwelo "Tests") |
836+
767837
<!-- ALL-CONTRIBUTORS-LIST:END -->
768838
769839
This project follows the [all-contributors][all-contributors] specification.

extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ declare namespace jest {
99
toBeEmpty(): R
1010
toBeDisabled(): R
1111
toBeEnabled(): R
12+
toBeRequired(): R
1213
toContainElement(element: HTMLElement | SVGElement | null): R
1314
toContainHTML(htmlText: string): R
1415
toHaveAttribute(attr: string, value?: any): R

src/__tests__/to-be-required.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {render} from './helpers/test-utils'
2+
3+
test('.toBeRequired', () => {
4+
const {queryByTestId} = render(`
5+
<div>
6+
<input data-testid="required-input" required>
7+
<input data-testid="aria-required-input" aria-required="true">
8+
<input data-testid="conflicted-input" required aria-required="false">
9+
<input data-testid="not-required-input" aria-required="false">
10+
<input data-testid="basic-input">
11+
<input data-testid="unsupported-type" type="image" required>
12+
<select data-testid="select" required></select>
13+
<textarea data-testid="textarea" required></textarea>
14+
<div data-testid="supported-role" role="tree" required></div>
15+
<div data-testid="supported-role-aria" role="tree" aria-required="true"></div>
16+
</div>
17+
`)
18+
19+
expect(queryByTestId('required-input')).toBeRequired()
20+
expect(queryByTestId('aria-required-input')).toBeRequired()
21+
expect(queryByTestId('conflicted-input')).toBeRequired()
22+
expect(queryByTestId('not-required-input')).not.toBeRequired()
23+
expect(queryByTestId('basic-input')).not.toBeRequired()
24+
expect(queryByTestId('unsupported-type')).not.toBeRequired()
25+
expect(queryByTestId('select')).toBeRequired()
26+
expect(queryByTestId('textarea')).toBeRequired()
27+
expect(queryByTestId('supported-role')).not.toBeRequired()
28+
expect(queryByTestId('supported-role-aria')).toBeRequired()
29+
30+
// negative test cases wrapped in throwError assertions for coverage.
31+
expect(() =>
32+
expect(queryByTestId('required-input')).not.toBeRequired(),
33+
).toThrowError()
34+
expect(() =>
35+
expect(queryByTestId('aria-required-input')).not.toBeRequired(),
36+
).toThrowError()
37+
expect(() =>
38+
expect(queryByTestId('conflicted-input')).not.toBeRequired(),
39+
).toThrowError()
40+
expect(() =>
41+
expect(queryByTestId('not-required-input')).toBeRequired(),
42+
).toThrowError()
43+
expect(() =>
44+
expect(queryByTestId('basic-input')).toBeRequired(),
45+
).toThrowError()
46+
expect(() =>
47+
expect(queryByTestId('unsupported-type')).toBeRequired(),
48+
).toThrowError()
49+
expect(() =>
50+
expect(queryByTestId('select')).not.toBeRequired(),
51+
).toThrowError()
52+
expect(() =>
53+
expect(queryByTestId('textarea')).not.toBeRequired(),
54+
).toThrowError()
55+
expect(() =>
56+
expect(queryByTestId('supported-role')).toBeRequired(),
57+
).toThrowError()
58+
expect(() =>
59+
expect(queryByTestId('supported-role-aria')).not.toBeRequired(),
60+
).toThrowError()
61+
})

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {toHaveFocus} from './to-have-focus'
1111
import {toHaveFormValues} from './to-have-form-values'
1212
import {toBeVisible} from './to-be-visible'
1313
import {toBeDisabled, toBeEnabled} from './to-be-disabled'
14+
import {toBeRequired} from './to-be-required'
1415

1516
export {
1617
toBeInTheDOM,
@@ -27,4 +28,5 @@ export {
2728
toBeVisible,
2829
toBeDisabled,
2930
toBeEnabled,
31+
toBeRequired,
3032
}

src/to-be-required.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {matcherHint, printReceived} from 'jest-matcher-utils'
2+
import {checkHtmlElement} from './utils'
3+
4+
// form elements that support 'required'
5+
const FORM_TAGS = ['select', 'textarea']
6+
7+
const ARIA_FORM_TAGS = ['input', 'select', 'textarea']
8+
9+
const UNSUPPORTED_INPUT_TYPES = [
10+
'color',
11+
'hidden',
12+
'range',
13+
'submit',
14+
'image',
15+
'reset',
16+
]
17+
18+
const SUPPORTED_ARIA_ROLES = [
19+
'combobox',
20+
'gridcell',
21+
'radiogroup',
22+
'spinbutton',
23+
'tree',
24+
]
25+
26+
function getTag(element) {
27+
return element.tagName && element.tagName.toLowerCase()
28+
}
29+
30+
function isRequiredOnFormTagsExceptInput(element) {
31+
return FORM_TAGS.includes(getTag(element)) && element.hasAttribute('required')
32+
}
33+
34+
function isRequiredOnSupportedInput(element) {
35+
return (
36+
getTag(element) === 'input' &&
37+
element.hasAttribute('required') &&
38+
((element.hasAttribute('type') &&
39+
!UNSUPPORTED_INPUT_TYPES.includes(element.getAttribute('type'))) ||
40+
!element.hasAttribute('type'))
41+
)
42+
}
43+
44+
function isElementRequiredByARIA(element) {
45+
return (
46+
element.hasAttribute('aria-required') &&
47+
element.getAttribute('aria-required') === 'true' &&
48+
(ARIA_FORM_TAGS.includes(getTag(element)) ||
49+
(element.hasAttribute('role') &&
50+
SUPPORTED_ARIA_ROLES.includes(element.getAttribute('role'))))
51+
)
52+
}
53+
54+
export function toBeRequired(element) {
55+
checkHtmlElement(element, toBeRequired, this)
56+
57+
const isRequired =
58+
isRequiredOnFormTagsExceptInput(element) ||
59+
isRequiredOnSupportedInput(element) ||
60+
isElementRequiredByARIA(element)
61+
62+
return {
63+
pass: isRequired,
64+
message: () => {
65+
const is = isRequired ? 'is' : 'is not'
66+
return [
67+
matcherHint(`${this.isNot ? '.not' : ''}.toBeRequired`, 'element', ''),
68+
'',
69+
`Received element ${is} required:`,
70+
` ${printReceived(element.cloneNode(false))}`,
71+
].join('\n')
72+
},
73+
}
74+
}

0 commit comments

Comments
 (0)