Skip to content

Commit b764bc9

Browse files
calladasmacpherson64
authored andcommitted
feat: toBeDisabled custom matcher (#18) (#19)
* feat: toBeDisabled custom matcher (#18) * Updated tests, removed nested divs * Updated documentation * toBeDisabled test moved to a separate file
1 parent 899945c commit b764bc9

File tree

6 files changed

+231
-2
lines changed

6 files changed

+231
-2
lines changed

.all-contributorsrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,17 @@
168168
"bug",
169169
"code"
170170
]
171+
},
172+
{
173+
"login": "callada",
174+
"name": "Iwona",
175+
"avatar_url": "https://avatars2.githubusercontent.com/u/7830590?v=4",
176+
"profile": "https://github.com/callada",
177+
"contributions": [
178+
"code",
179+
"doc",
180+
"test"
181+
]
171182
}
172183
]
173184
}

README.md

Lines changed: 35 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-16-orange.svg?style=flat-square)](#contributors)
19+
[![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors)
2020
[![PRs Welcome][prs-badge]][prs]
2121
[![Code of Conduct][coc-badge]][coc]
2222

@@ -54,6 +54,7 @@ to maintain.
5454
- [`toHaveStyle`](#tohavestyle)
5555
- [`toHaveFocus`](#tohavefocus)
5656
- [`toBeVisible`](#tobevisible)
57+
- [`toBeDisabled`](#tobedisabled)
5758
- [Inspiration](#inspiration)
5859
- [Other Solutions](#other-solutions)
5960
- [Guiding Principles](#guiding-principles)
@@ -305,6 +306,38 @@ expect(container.querySelector('strong')).not.toBeVisible()
305306
// ...
306307
```
307308

309+
### `toBeDisabled`
310+
311+
This allows you to check whether an element is disabled from the user's perspective.
312+
313+
It matches if the element is a form control and the `disabled` attribute is
314+
specified on this element or the element is a descendant of a form element
315+
with a `disabled` attribute.
316+
317+
According to the specification, the following elements can be [actually disabled](https://html.spec.whatwg.org/multipage/semantics-other.html#disabled-elements):
318+
`button`, `input`, `select`, `textarea`, `optgroup`, `option`, `fieldset`.
319+
320+
```javascript
321+
// add the custom expect matchers once
322+
import 'jest-dom/extend-expect'
323+
324+
// ...
325+
// <button data-testid="button" type="submit" disabled>
326+
// SUBMIT
327+
// </button>
328+
// <fieldset disabled>
329+
// <input type="text" data-testid="text" />
330+
// </fieldset>
331+
expect(getByTestId(container, 'button')).toBeDisabled()
332+
expect(getByTestId(container, 'text')).toBeDisabled()
333+
// ...
334+
335+
// ...
336+
// <a href="..." disabled>LINK</a>
337+
expect(getByText(container, 'LINK')).not.toBeDisabled()
338+
// ...
339+
```
340+
308341
## Inspiration
309342

310343
This whole library was extracted out of Kent C. Dodds' [dom-testing-library][],
@@ -343,7 +376,7 @@ Thanks goes to these people ([emoji key][emojis]):
343376
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=kentcdodds "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/gnapse/jest-dom/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/gnapse/jest-dom/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/gnapse/jest-dom/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[🐛](https://github.com/gnapse/jest-dom/issues?q=author%3Apbomb "Bug reports") [💻](https://github.com/gnapse/jest-dom/commits?author=pbomb "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=pbomb "Documentation") [⚠️](https://github.com/gnapse/jest-dom/commits?author=pbomb "Tests") | [<img src="https://avatars1.githubusercontent.com/u/1288694?v=4" width="100px;"/><br /><sub><b>Justin Hall</b></sub>](https://github.com/wKovacs64)<br />[📦](#platform-wKovacs64 "Packaging/porting to new platform") |
344377
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
345378
| [<img src="https://avatars1.githubusercontent.com/u/1241511?s=460&v=4" width="100px;"/><br /><sub><b>Anto Aravinth</b></sub>](https://github.com/antoaravinth)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=antoaravinth "Code") [⚠️](https://github.com/gnapse/jest-dom/commits?author=antoaravinth "Tests") [📖](https://github.com/gnapse/jest-dom/commits?author=antoaravinth "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/3462296?v=4" width="100px;"/><br /><sub><b>Jonah Moses</b></sub>](https://github.com/JonahMoses)<br />[📖](https://github.com/gnapse/jest-dom/commits?author=JonahMoses "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/4002543?v=4" width="100px;"/><br /><sub><b>Łukasz Gandecki</b></sub>](http://team.thebrain.pro)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=lgandecki "Code") [⚠️](https://github.com/gnapse/jest-dom/commits?author=lgandecki "Tests") [📖](https://github.com/gnapse/jest-dom/commits?author=lgandecki "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/498274?v=4" width="100px;"/><br /><sub><b>Ivan Babak</b></sub>](https://sompylasar.github.io)<br />[🐛](https://github.com/gnapse/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;"/><br /><sub><b>Jesse Day</b></sub>](https://github.com/jday3)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=jday3 "Code") | [<img src="https://avatars0.githubusercontent.com/u/15199?v=4" width="100px;"/><br /><sub><b>Ernesto García</b></sub>](http://gnapse.github.io)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=gnapse "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=gnapse "Documentation") [⚠️](https://github.com/gnapse/jest-dom/commits?author=gnapse "Tests") | [<img src="https://avatars0.githubusercontent.com/u/79312?v=4" width="100px;"/><br /><sub><b>Mark Volkmann</b></sub>](http://ociweb.com/mark/)<br />[🐛](https://github.com/gnapse/jest-dom/issues?q=author%3Amvolkmann "Bug reports") [💻](https://github.com/gnapse/jest-dom/commits?author=mvolkmann "Code") |
346-
| [<img src="https://avatars1.githubusercontent.com/u/1659099?v=4" width="100px;"/><br /><sub><b>smacpherson64</b></sub>](https://github.com/smacpherson64)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Documentation") [⚠️](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Tests") | [<img src="https://avatars2.githubusercontent.com/u/132233?v=4" width="100px;"/><br /><sub><b>John Gozde</b></sub>](https://github.com/jgoz)<br />[🐛](https://github.com/gnapse/jest-dom/issues?q=author%3Ajgoz "Bug reports") [💻](https://github.com/gnapse/jest-dom/commits?author=jgoz "Code") |
379+
| [<img src="https://avatars1.githubusercontent.com/u/1659099?v=4" width="100px;"/><br /><sub><b>smacpherson64</b></sub>](https://github.com/smacpherson64)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Documentation") [⚠️](https://github.com/gnapse/jest-dom/commits?author=smacpherson64 "Tests") | [<img src="https://avatars2.githubusercontent.com/u/132233?v=4" width="100px;"/><br /><sub><b>John Gozde</b></sub>](https://github.com/jgoz)<br />[🐛](https://github.com/gnapse/jest-dom/issues?q=author%3Ajgoz "Bug reports") [💻](https://github.com/gnapse/jest-dom/commits?author=jgoz "Code") | [<img src="https://avatars2.githubusercontent.com/u/7830590?v=4" width="100px;"/><br /><sub><b>Iwona</b></sub>](https://github.com/callada)<br />[💻](https://github.com/gnapse/jest-dom/commits?author=callada "Code") [📖](https://github.com/gnapse/jest-dom/commits?author=callada "Documentation") [⚠️](https://github.com/gnapse/jest-dom/commits?author=callada "Tests") |
347380

348381
<!-- ALL-CONTRIBUTORS-LIST:END -->
349382

extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ declare namespace jest {
33
toBeInTheDOM(container?: HTMLElement): R
44
toBeVisible(): R
55
toBeEmpty(): R
6+
toBeDisabled(): R
67
toContainElement(element: HTMLElement): R
78
toHaveAttribute(attr: string, value?: string): R
89
toHaveClass(...classNames: string[]): R

src/__tests__/to-be-disabled.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import '../extend-expect'
2+
import {plugins} from 'pretty-format'
3+
import {render} from './helpers/test-utils'
4+
5+
expect.addSnapshotSerializer(plugins.ConvertAnsi)
6+
7+
test('.toBeDisabled', () => {
8+
const {queryByTestId} = render(`
9+
<div>
10+
<button disabled={true} data-testid="button-element">x</button>
11+
<textarea disabled={true} data-testid="textarea-element"></textarea>
12+
<input type="checkbox" disabled={true} data-testid="input-element" />
13+
14+
<fieldset disabled={true} data-testid="fieldset-element">
15+
<button data-testid="fieldset-child-element">x</button>
16+
</fieldset>
17+
18+
<div disabled={true} data-testid="div-element">
19+
<button data-testid="div-child-element">x</button>
20+
</div>
21+
22+
<fieldset disabled={true}>
23+
<div>
24+
<button data-testid="nested-form-element">x</button>
25+
26+
<select data-testid="deep-select-element">
27+
<optgroup data-testid="deep-optgroup-element">
28+
<option data-testid="deep-option-element">x</option>
29+
</optgroup>
30+
</select>
31+
</div>
32+
</fieldset>
33+
34+
<a href="http://github.com" disabled={true} data-testid="a-element">x</a>
35+
</div>
36+
`)
37+
38+
expect(queryByTestId('button-element')).toBeDisabled()
39+
expect(() =>
40+
expect(queryByTestId('button-element')).not.toBeDisabled(),
41+
).toThrowError()
42+
expect(queryByTestId('textarea-element')).toBeDisabled()
43+
expect(queryByTestId('input-element')).toBeDisabled()
44+
45+
expect(queryByTestId('fieldset-element')).toBeDisabled()
46+
expect(queryByTestId('fieldset-child-element')).toBeDisabled()
47+
48+
expect(queryByTestId('div-element')).not.toBeDisabled()
49+
expect(queryByTestId('div-child-element')).not.toBeDisabled()
50+
51+
expect(queryByTestId('nested-form-element')).toBeDisabled()
52+
expect(queryByTestId('deep-select-element')).toBeDisabled()
53+
expect(queryByTestId('deep-optgroup-element')).toBeDisabled()
54+
expect(queryByTestId('deep-option-element')).toBeDisabled()
55+
56+
expect(queryByTestId('a-element')).not.toBeDisabled()
57+
expect(() => expect(queryByTestId('a-element')).toBeDisabled()).toThrowError()
58+
})
59+
60+
test('.toBeDisabled fieldset>legend', () => {
61+
const {queryByTestId} = render(`
62+
<div>
63+
<fieldset disabled={true}>
64+
<button data-testid="inherited-element">x</button>
65+
</fieldset>
66+
67+
<fieldset disabled={true}>
68+
<legend>
69+
<button data-testid="inside-legend-element">x</button>
70+
</legend>
71+
</fieldset>
72+
73+
<fieldset disabled={true}>
74+
<legend>
75+
<div>
76+
<button data-testid="nested-inside-legend-element">x</button>
77+
</div>
78+
</legend>
79+
</fieldset>
80+
81+
<fieldset disabled={true}>
82+
<div></div>
83+
<legend>
84+
<button data-testid="first-legend-element">x</button>
85+
</legend>
86+
<legend>
87+
<button data-testid="second-legend-element">x</button>
88+
</legend>
89+
</fieldset>
90+
91+
<fieldset disabled={true}>
92+
<fieldset>
93+
<legend>
94+
<button data-testid="outer-fieldset-element">x</button>
95+
</legend>
96+
</fieldset>
97+
</fieldset>
98+
</div>
99+
`)
100+
101+
expect(queryByTestId('inherited-element')).toBeDisabled()
102+
expect(queryByTestId('inside-legend-element')).not.toBeDisabled()
103+
expect(queryByTestId('nested-inside-legend-element')).not.toBeDisabled()
104+
105+
expect(queryByTestId('first-legend-element')).not.toBeDisabled()
106+
expect(queryByTestId('second-legend-element')).toBeDisabled()
107+
108+
expect(queryByTestId('outer-fieldset-element')).toBeDisabled()
109+
})

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {toHaveClass} from './to-have-class'
77
import {toHaveStyle} from './to-have-style'
88
import {toHaveFocus} from './to-have-focus'
99
import {toBeVisible} from './to-be-visible'
10+
import {toBeDisabled} from './to-be-disabled'
1011

1112
export {
1213
toBeInTheDOM,
@@ -18,4 +19,5 @@ export {
1819
toHaveStyle,
1920
toHaveFocus,
2021
toBeVisible,
22+
toBeDisabled,
2123
}

src/to-be-disabled.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {matcherHint, printReceived} from 'jest-matcher-utils'
2+
import {checkHtmlElement} from './utils'
3+
4+
// form elements that support 'disabled'
5+
const FORM_TAGS = [
6+
'fieldset',
7+
'input',
8+
'select',
9+
'optgroup',
10+
'option',
11+
'button',
12+
'textarea',
13+
]
14+
15+
function getTag(element) {
16+
return element.tagName && element.tagName.toLowerCase()
17+
}
18+
19+
/*
20+
* According to specification:
21+
* If <fieldset> is disabled, the form controls that are its descendants,
22+
* except descendants of its first optional <legend> element, are disabled
23+
*
24+
* https://html.spec.whatwg.org/multipage/form-elements.html#concept-fieldset-disabled
25+
*
26+
* This method tests whether element is first legend child of fieldset parent
27+
*/
28+
function isFirstLegendChildOfFieldset(element, parent) {
29+
return (
30+
getTag(element) === 'legend' &&
31+
getTag(parent) === 'fieldset' &&
32+
element.isSameNode(
33+
Array.from(parent.children).find(child => getTag(child) === 'legend'),
34+
)
35+
)
36+
}
37+
38+
function isElementDisabledByParent(element, parent) {
39+
return (
40+
isElementDisabled(parent) && !isFirstLegendChildOfFieldset(element, parent)
41+
)
42+
}
43+
44+
function isElementDisabled(element) {
45+
return FORM_TAGS.includes(getTag(element)) && element.hasAttribute('disabled')
46+
}
47+
48+
function isAncestorDisabled(element) {
49+
const parent = element.parentElement
50+
return (
51+
Boolean(parent) &&
52+
(isElementDisabledByParent(element, parent) || isAncestorDisabled(parent))
53+
)
54+
}
55+
56+
export function toBeDisabled(element) {
57+
checkHtmlElement(element, toBeDisabled, this)
58+
59+
const isDisabled = isElementDisabled(element) || isAncestorDisabled(element)
60+
61+
return {
62+
pass: isDisabled,
63+
message: () => {
64+
const is = isDisabled ? 'is' : 'is not'
65+
return [
66+
matcherHint(`${this.isNot ? '.not' : ''}.toBeDisabled`, 'element', ''),
67+
'',
68+
`Received element ${is} disabled:`,
69+
` ${printReceived(element.cloneNode(false))}`,
70+
].join('\n')
71+
},
72+
}
73+
}

0 commit comments

Comments
 (0)