Skip to content

Commit

Permalink
feat: extend toBeChecked to support any role that's compatible (#267)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tf authored Jun 25, 2020
1 parent 0428c5c commit c135d0b
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 8 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^4.2.2",
"chalk": "^3.0.0",
"css": "^2.2.4",
"css.escape": "^1.5.1",
Expand All @@ -55,7 +56,9 @@
},
"overrides": [
{
"files": ["src/__tests__/*.js"],
"files": [
"src/__tests__/*.js"
],
"rules": {
"max-lines-per-function": "off"
}
Expand Down
18 changes: 14 additions & 4 deletions src/__tests__/to-be-checked.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ describe('.toBeChecked', () => {
expect(queryByTestId('aria-switch-unchecked')).not.toBeChecked()
})

test('handles element with role="menuitemcheckbox"', () => {
const {queryByTestId} = render(`
<div role="menuitemcheckbox" aria-checked="true" data-testid="aria-menuitemcheckbox-checked" />
<div role="menuitemcheckbox" aria-checked="false" data-testid="aria-menuitemcheckbox-unchecked" />
`)

expect(queryByTestId('aria-menuitemcheckbox-checked')).toBeChecked()
expect(queryByTestId('aria-menuitemcheckbox-unchecked')).not.toBeChecked()
})

test('throws when checkbox input is checked but expected not to be', () => {
const {queryByTestId} = render(
`<input type="checkbox" checked data-testid="input-checked" />`,
Expand Down Expand Up @@ -159,7 +169,7 @@ describe('.toBeChecked', () => {
expect(() =>
expect(queryByTestId('aria-checkbox-invalid')).toBeChecked(),
).toThrowError(
'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',
/only inputs with .* a valid aria-checked attribute can be used/,
)
})

Expand All @@ -171,7 +181,7 @@ describe('.toBeChecked', () => {
expect(() =>
expect(queryByTestId('aria-radio-invalid')).toBeChecked(),
).toThrowError(
'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',
/only inputs with .* a valid aria-checked attribute can be used/,
)
})

Expand All @@ -183,14 +193,14 @@ describe('.toBeChecked', () => {
expect(() =>
expect(queryByTestId('aria-switch-invalid')).toBeChecked(),
).toThrowError(
'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',
/only inputs with .* a valid aria-checked attribute can be used/,
)
})

test('throws when the element is not an input', () => {
const {queryByTestId} = render(`<select data-testid="select"></select>`)
expect(() => expect(queryByTestId('select')).toBeChecked()).toThrowError(
'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',
/only inputs with type="checkbox" or type="radio" or elements with.* role="checkbox".* role="menuitemcheckbox".* role="radio".* role="switch" .* can be used/,
)
})
})
27 changes: 27 additions & 0 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
checkHtmlElement,
HtmlElementTypeError,
parseJStoCSS,
toSentence,
} from '../utils'
import document from './helpers/document'

Expand Down Expand Up @@ -116,3 +117,29 @@ describe('parseJStoCSS', () => {
})
})
})

describe('toSentence', () => {
it('turns array into string of comma separated list with default last word connector', () => {
expect(toSentence(['one', 'two', 'three'])).toBe('one, two and three')
})

it('supports custom word connector', () => {
expect(toSentence(['one', 'two', 'three'], {wordConnector: '; '})).toBe(
'one; two and three',
)
})

it('supports custom last word connector', () => {
expect(
toSentence(['one', 'two', 'three'], {lastWordConnector: ' or '}),
).toBe('one, two or three')
})

it('turns one element array into string containing first element', () => {
expect(toSentence(['one'])).toBe('one')
})

it('turns empty array into empty string', () => {
expect(toSentence([])).toBe('')
})
})
22 changes: 19 additions & 3 deletions src/to-be-checked.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {roles} from 'aria-query'
import {matcherHint, printReceived} from 'jest-matcher-utils'
import {checkHtmlElement} from './utils'
import {checkHtmlElement, toSentence} from './utils'

export function toBeChecked(element) {
checkHtmlElement(element, toBeChecked, this)
Expand All @@ -13,7 +14,7 @@ export function toBeChecked(element) {

const isValidAriaElement = () => {
return (
['checkbox', 'radio', 'switch'].includes(element.getAttribute('role')) &&
roleSupportsChecked(element.getAttribute('role')) &&
['true', 'false'].includes(element.getAttribute('aria-checked'))
)
}
Expand All @@ -22,7 +23,7 @@ export function toBeChecked(element) {
return {
pass: false,
message: () =>
'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',
`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`,
}
}

Expand All @@ -44,3 +45,18 @@ export function toBeChecked(element) {
},
}
}

function supportedRolesSentence() {
return toSentence(
supportedRoles().map(role => `role="${role}"`),
{lastWordConnector: ' or '},
)
}

function supportedRoles() {
return Array.from(roles.keys()).filter(roleSupportsChecked)
}

function roleSupportsChecked(role) {
return roles.get(role)?.props['aria-checked'] !== undefined
}
10 changes: 10 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ function parseJStoCSS(document, css) {
return sandboxElement.style.cssText
}

function toSentence(
array,
{wordConnector = ', ', lastWordConnector = ' and '} = {},
) {
return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join(
array.length > 1 ? lastWordConnector : '',
)
}

export {
HtmlElementTypeError,
checkHtmlElement,
Expand All @@ -210,4 +219,5 @@ export {
getSingleElementValue,
compareArraysAsSet,
parseJStoCSS,
toSentence,
}

0 comments on commit c135d0b

Please sign in to comment.