Skip to content

Commit dbbea6e

Browse files
eps1lonKent C. Dodds
authored and
Kent C. Dodds
committed
feat(byRole): Exclude inaccessible elements (#352)
* Add acceptance tests * Implement hidden: false * Distinguish error message for hidden * Fix double space * Add hint about possible inaccessible roles * Add tests for hidden: true * Fix codecoverage * Use hidden property instead of manually reflecting boolean attributes
1 parent 34bed8e commit dbbea6e

File tree

4 files changed

+270
-39
lines changed

4 files changed

+270
-39
lines changed

src/__tests__/element-queries.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ test('get throws a useful error message', () => {
8686
</div>"
8787
`)
8888
expect(() => getByRole('LucyRicardo')).toThrowErrorMatchingInlineSnapshot(`
89-
"Unable to find an element with the role "LucyRicardo"
89+
"Unable to find an accessible element with the role "LucyRicardo"
9090
91-
There are no available roles.
91+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
9292
9393
<div>
9494
<div />

src/__tests__/role.js

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {render} from './helpers/test-utils'
22

3-
test('logs available roles when it fails', () => {
3+
test('by default logs accessible roles when it fails', () => {
44
const {getByRole} = render(`<h1>Hi</h1>`)
55
expect(() => getByRole('article')).toThrowErrorMatchingInlineSnapshot(`
6-
"Unable to find an element with the role "article"
6+
"Unable to find an accessible element with the role "article"
77
8-
Here are the available roles:
8+
Here are the accessible roles:
99
1010
heading:
1111
@@ -21,9 +21,49 @@ Here are the available roles:
2121
`)
2222
})
2323

24-
test('logs error when there are no available roles', () => {
24+
test('when hidden: true logs available roles when it fails', () => {
25+
const {getByRole} = render(`<div hidden><h1>Hi</h1></div>`)
26+
expect(() => getByRole('article', {hidden: true}))
27+
.toThrowErrorMatchingInlineSnapshot(`
28+
"Unable to find an element with the role "article"
29+
30+
Here are the available roles:
31+
32+
heading:
33+
34+
<h1 />
35+
36+
--------------------------------------------------
37+
38+
<div>
39+
<div
40+
hidden=""
41+
>
42+
<h1>
43+
Hi
44+
</h1>
45+
</div>
46+
</div>"
47+
`)
48+
})
49+
50+
test('logs error when there are no accessible roles', () => {
2551
const {getByRole} = render('<div />')
2652
expect(() => getByRole('article')).toThrowErrorMatchingInlineSnapshot(`
53+
"Unable to find an accessible element with the role "article"
54+
55+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
56+
57+
<div>
58+
<div />
59+
</div>"
60+
`)
61+
})
62+
63+
test('logs a different error if inaccessible roles should be included', () => {
64+
const {getByRole} = render('<div />')
65+
expect(() => getByRole('article', {hidden: true}))
66+
.toThrowErrorMatchingInlineSnapshot(`
2767
"Unable to find an element with the role "article"
2868
2969
There are no available roles.
@@ -33,3 +73,109 @@ There are no available roles.
3373
</div>"
3474
`)
3575
})
76+
77+
test('by default excludes elements that have the html hidden attribute or any of their parents', () => {
78+
const {getByRole} = render('<div hidden><ul /></div>')
79+
80+
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
81+
"Unable to find an accessible element with the role "list"
82+
83+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
84+
85+
<div>
86+
<div
87+
hidden=""
88+
>
89+
<ul />
90+
</div>
91+
</div>"
92+
`)
93+
})
94+
95+
test('by default excludes elements which have display: none or any of their parents', () => {
96+
const {getByRole} = render(
97+
'<div style="display: none;"><ul style="display: block;" /></div>',
98+
)
99+
100+
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
101+
"Unable to find an accessible element with the role "list"
102+
103+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
104+
105+
<div>
106+
<div
107+
style="display: none;"
108+
>
109+
<ul
110+
style="display: block;"
111+
/>
112+
</div>
113+
</div>"
114+
`)
115+
})
116+
117+
test('by default excludes elements which have visibility hidden', () => {
118+
const {getByRole} = render('<div style="visibility: hidden;"><ul /></div>')
119+
120+
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
121+
"Unable to find an accessible element with the role "list"
122+
123+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
124+
125+
<div>
126+
<div
127+
style="visibility: hidden;"
128+
>
129+
<ul />
130+
</div>
131+
</div>"
132+
`)
133+
})
134+
135+
test('by default excludes elements which have aria-hidden="true" or any of their parents', () => {
136+
// > if it, or any of its ancestors [...] have their aria-hidden attribute value set to true.
137+
// -- https://www.w3.org/TR/wai-aria/#aria-hidden
138+
// > In other words, aria-hidden="true" on a parent overrides aria-hidden="false" on descendants.
139+
// -- https://www.w3.org/TR/core-aam-1.1/#exclude_elements2
140+
const {getByRole} = render(
141+
'<div aria-hidden="true"><ul aria-hidden="false" /></div>',
142+
)
143+
144+
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
145+
"Unable to find an accessible element with the role "list"
146+
147+
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
148+
149+
<div>
150+
<div
151+
aria-hidden="true"
152+
>
153+
<ul
154+
aria-hidden="false"
155+
/>
156+
</div>
157+
</div>"
158+
`)
159+
})
160+
161+
test('considers the computed visibility style not the parent', () => {
162+
// this behavior deviates from the spec which includes "any descendant"
163+
// if visibility is hidden. However, chrome a11y tree and nvda will include
164+
// the following markup. This behavior might change depending on how
165+
// https://github.com/w3c/aria/issues/1055 is resolved.
166+
const {getByRole} = render(
167+
'<div style="visibility: hidden;"><main style="visibility: visible;"><ul /></main></div>',
168+
)
169+
170+
expect(getByRole('list')).not.toBeNull()
171+
})
172+
173+
test('can include inaccessible roles', () => {
174+
// this behavior deviates from the spec which includes "any descendant"
175+
// if visibility is hidden. However, chrome a11y tree and nvda will include
176+
// the following markup. This behavior might change depending on how
177+
// https://github.com/w3c/aria/issues/1055 is resolved.
178+
const {getByRole} = render('<div hidden><ul /></div>')
179+
180+
expect(getByRole('list', {hidden: true})).not.toBeNull()
181+
})

src/queries/role.js

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,67 @@
1-
import {getImplicitAriaRoles, prettyRoles} from '../role-helpers'
1+
import {
2+
getImplicitAriaRoles,
3+
prettyRoles,
4+
shouldExcludeFromA11yTree,
5+
} from '../role-helpers'
26
import {buildQueries, fuzzyMatches, makeNormalizer, matches} from './all-utils'
37

48
function queryAllByRole(
59
container,
610
role,
7-
{exact = true, collapseWhitespace, trim, normalizer} = {},
11+
{exact = true, collapseWhitespace, hidden = false, trim, normalizer} = {},
812
) {
913
const matcher = exact ? matches : fuzzyMatches
1014
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
1115

12-
return Array.from(container.querySelectorAll('*')).filter(node => {
13-
const isRoleSpecifiedExplicitly = node.hasAttribute('role')
16+
return Array.from(container.querySelectorAll('*'))
17+
.filter(element => {
18+
return hidden === false
19+
? shouldExcludeFromA11yTree(element) === false
20+
: true
21+
})
22+
.filter(node => {
23+
const isRoleSpecifiedExplicitly = node.hasAttribute('role')
1424

15-
if (isRoleSpecifiedExplicitly) {
16-
return matcher(node.getAttribute('role'), node, role, matchNormalizer)
17-
}
25+
if (isRoleSpecifiedExplicitly) {
26+
return matcher(node.getAttribute('role'), node, role, matchNormalizer)
27+
}
1828

19-
const implicitRoles = getImplicitAriaRoles(node)
29+
const implicitRoles = getImplicitAriaRoles(node)
2030

21-
return implicitRoles.some(implicitRole =>
22-
matcher(implicitRole, node, role, matchNormalizer),
23-
)
24-
})
31+
return implicitRoles.some(implicitRole =>
32+
matcher(implicitRole, node, role, matchNormalizer),
33+
)
34+
})
2535
}
2636

2737
const getMultipleError = (c, role) =>
2838
`Found multiple elements with the role "${role}"`
2939

30-
const getMissingError = (container, role) => {
31-
const roles = prettyRoles(container)
40+
const getMissingError = (container, role, {hidden = false} = {}) => {
41+
const roles = prettyRoles(container, {hidden})
3242
let roleMessage
3343

3444
if (roles.length === 0) {
35-
roleMessage = 'There are no available roles.'
45+
if (hidden === false) {
46+
roleMessage =
47+
'There are no accessible roles. But there might be some inaccessible roles. ' +
48+
'If you wish to access them, then set the `hidden` option to `true`. ' +
49+
'Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole'
50+
} else {
51+
roleMessage = 'There are no available roles.'
52+
}
3653
} else {
3754
roleMessage = `
38-
Here are the available roles:
55+
Here are the ${hidden === false ? 'accessible' : 'available'} roles:
3956
4057
${roles.replace(/\n/g, '\n ').replace(/\n\s\s\n/g, '\n\n')}
4158
`.trim()
4259
}
4360

4461
return `
45-
Unable to find an element with the role "${role}"
62+
Unable to find an ${
63+
hidden === false ? 'accessible ' : ''
64+
}element with the role "${role}"
4665
4766
${roleMessage}`.trim()
4867
}

0 commit comments

Comments
 (0)