Skip to content

Commit c135d0b

Browse files
authored
feat: extend toBeChecked to support any role that's compatible (#267)
* Get supported roles for toBeChecked via aria-query Instead of hard coding a list of roles, use information provided by the `aria-query` package to determine whether an element with a given role supports the `aria-checked` attribute. Dynamically generate an error message listing all supported roles. Make error message expectations more fuzzy to prevent test failures if the list of supported roles should ever change.
1 parent 0428c5c commit c135d0b

File tree

5 files changed

+74
-8
lines changed

5 files changed

+74
-8
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"dependencies": {
3434
"@babel/runtime": "^7.9.2",
3535
"@types/testing-library__jest-dom": "^5.9.1",
36+
"aria-query": "^4.2.2",
3637
"chalk": "^3.0.0",
3738
"css": "^2.2.4",
3839
"css.escape": "^1.5.1",
@@ -55,7 +56,9 @@
5556
},
5657
"overrides": [
5758
{
58-
"files": ["src/__tests__/*.js"],
59+
"files": [
60+
"src/__tests__/*.js"
61+
],
5962
"rules": {
6063
"max-lines-per-function": "off"
6164
}

src/__tests__/to-be-checked.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ describe('.toBeChecked', () => {
5151
expect(queryByTestId('aria-switch-unchecked')).not.toBeChecked()
5252
})
5353

54+
test('handles element with role="menuitemcheckbox"', () => {
55+
const {queryByTestId} = render(`
56+
<div role="menuitemcheckbox" aria-checked="true" data-testid="aria-menuitemcheckbox-checked" />
57+
<div role="menuitemcheckbox" aria-checked="false" data-testid="aria-menuitemcheckbox-unchecked" />
58+
`)
59+
60+
expect(queryByTestId('aria-menuitemcheckbox-checked')).toBeChecked()
61+
expect(queryByTestId('aria-menuitemcheckbox-unchecked')).not.toBeChecked()
62+
})
63+
5464
test('throws when checkbox input is checked but expected not to be', () => {
5565
const {queryByTestId} = render(
5666
`<input type="checkbox" checked data-testid="input-checked" />`,
@@ -159,7 +169,7 @@ describe('.toBeChecked', () => {
159169
expect(() =>
160170
expect(queryByTestId('aria-checkbox-invalid')).toBeChecked(),
161171
).toThrowError(
162-
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox", role="radio" or role="switch" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
172+
/only inputs with .* a valid aria-checked attribute can be used/,
163173
)
164174
})
165175

@@ -171,7 +181,7 @@ describe('.toBeChecked', () => {
171181
expect(() =>
172182
expect(queryByTestId('aria-radio-invalid')).toBeChecked(),
173183
).toThrowError(
174-
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox", role="radio" or role="switch" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
184+
/only inputs with .* a valid aria-checked attribute can be used/,
175185
)
176186
})
177187

@@ -183,14 +193,14 @@ describe('.toBeChecked', () => {
183193
expect(() =>
184194
expect(queryByTestId('aria-switch-invalid')).toBeChecked(),
185195
).toThrowError(
186-
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox", role="radio" or role="switch" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
196+
/only inputs with .* a valid aria-checked attribute can be used/,
187197
)
188198
})
189199

190200
test('throws when the element is not an input', () => {
191201
const {queryByTestId} = render(`<select data-testid="select"></select>`)
192202
expect(() => expect(queryByTestId('select')).toBeChecked()).toThrowError(
193-
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox", role="radio" or role="switch" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
203+
/only inputs with type="checkbox" or type="radio" or elements with.* role="checkbox".* role="menuitemcheckbox".* role="radio".* role="switch" .* can be used/,
194204
)
195205
})
196206
})

src/__tests__/utils.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkHtmlElement,
44
HtmlElementTypeError,
55
parseJStoCSS,
6+
toSentence,
67
} from '../utils'
78
import document from './helpers/document'
89

@@ -116,3 +117,29 @@ describe('parseJStoCSS', () => {
116117
})
117118
})
118119
})
120+
121+
describe('toSentence', () => {
122+
it('turns array into string of comma separated list with default last word connector', () => {
123+
expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three')
124+
})
125+
126+
it('supports custom word connector', () => {
127+
expect(toSentence(['one', 'two', 'three'], {wordConnector: '; '})).toBe(
128+
'one; two and three',
129+
)
130+
})
131+
132+
it('supports custom last word connector', () => {
133+
expect(
134+
toSentence(['one', 'two', 'three'], {lastWordConnector: ' or '}),
135+
).toBe('one, two or three')
136+
})
137+
138+
it('turns one element array into string containing first element', () => {
139+
expect(toSentence(['one'])).toBe('one')
140+
})
141+
142+
it('turns empty array into empty string', () => {
143+
expect(toSentence([])).toBe('')
144+
})
145+
})

src/to-be-checked.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import {roles} from 'aria-query'
12
import {matcherHint, printReceived} from 'jest-matcher-utils'
2-
import {checkHtmlElement} from './utils'
3+
import {checkHtmlElement, toSentence} from './utils'
34

45
export function toBeChecked(element) {
56
checkHtmlElement(element, toBeChecked, this)
@@ -13,7 +14,7 @@ export function toBeChecked(element) {
1314

1415
const isValidAriaElement = () => {
1516
return (
16-
['checkbox', 'radio', 'switch'].includes(element.getAttribute('role')) &&
17+
roleSupportsChecked(element.getAttribute('role')) &&
1718
['true', 'false'].includes(element.getAttribute('aria-checked'))
1819
)
1920
}
@@ -22,7 +23,7 @@ export function toBeChecked(element) {
2223
return {
2324
pass: false,
2425
message: () =>
25-
'only inputs with type="checkbox" or type="radio" or elements with role="checkbox", role="radio" or role="switch" and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead',
26+
`only inputs with type="checkbox" or type="radio" or elements with ${supportedRolesSentence()} and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead`,
2627
}
2728
}
2829

@@ -44,3 +45,18 @@ export function toBeChecked(element) {
4445
},
4546
}
4647
}
48+
49+
function supportedRolesSentence() {
50+
return toSentence(
51+
supportedRoles().map(role => `role="${role}"`),
52+
{lastWordConnector: ' or '},
53+
)
54+
}
55+
56+
function supportedRoles() {
57+
return Array.from(roles.keys()).filter(roleSupportsChecked)
58+
}
59+
60+
function roleSupportsChecked(role) {
61+
return roles.get(role)?.props['aria-checked'] !== undefined
62+
}

src/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ function parseJStoCSS(document, css) {
198198
return sandboxElement.style.cssText
199199
}
200200

201+
function toSentence(
202+
array,
203+
{wordConnector = ', ', lastWordConnector = ' and '} = {},
204+
) {
205+
return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join(
206+
array.length > 1 ? lastWordConnector : '',
207+
)
208+
}
209+
201210
export {
202211
HtmlElementTypeError,
203212
checkHtmlElement,
@@ -210,4 +219,5 @@ export {
210219
getSingleElementValue,
211220
compareArraysAsSet,
212221
parseJStoCSS,
222+
toSentence,
213223
}

0 commit comments

Comments
 (0)