Skip to content

Commit 84c7290

Browse files
PaquitoSofttimdeschryvereps1lon
authored
feat(byRole): Add description filter (#1120)
Co-authored-by: Tim Deschryver <[email protected]> Co-authored-by: Sebastian Silbermann <[email protected]>
1 parent 6b99a7e commit 84c7290

File tree

4 files changed

+232
-5
lines changed

4 files changed

+232
-5
lines changed

src/__tests__/role.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,177 @@ test('should find the input using type property instead of attribute', () => {
572572
const {getByRole} = render('<input type="124">')
573573
expect(getByRole('textbox')).not.toBeNull()
574574
})
575+
576+
test('can be filtered by accessible description', () => {
577+
const targetedNotificationMessage = 'Your session is about to expire!'
578+
const {getByRole} = renderIntoDocument(
579+
`
580+
<ul>
581+
<li role="alertdialog" aria-describedby="notification-id-1">
582+
<div><button>Close</button></div>
583+
<div id="notification-id-1">You have unread emails</div>
584+
</li>
585+
<li role="alertdialog" aria-describedby="notification-id-2">
586+
<div><button>Close</button></div>
587+
<div id="notification-id-2">${targetedNotificationMessage}</div>
588+
</li>
589+
</ul>`,
590+
)
591+
592+
const notification = getByRole('alertdialog', {
593+
description: targetedNotificationMessage,
594+
})
595+
596+
expect(notification).not.toBeNull()
597+
expect(notification).toHaveTextContent(targetedNotificationMessage)
598+
599+
expect(
600+
getQueriesForElement(notification).getByRole('button', {name: 'Close'}),
601+
).not.toBeNull()
602+
})
603+
604+
test('error should include description when filtering and no results are found', () => {
605+
const targetedNotificationMessage = 'Your session is about to expire!'
606+
const {getByRole} = renderIntoDocument(
607+
`<div role="dialog" aria-describedby="some-id"><div><button>Close</button></div><div id="some-id">${targetedNotificationMessage}</div></div>`,
608+
)
609+
610+
expect(() =>
611+
getByRole('alertdialog', {description: targetedNotificationMessage}),
612+
).toThrowErrorMatchingInlineSnapshot(`
613+
Unable to find an accessible element with the role "alertdialog" and description "Your session is about to expire!"
614+
615+
Here are the accessible roles:
616+
617+
dialog:
618+
619+
Name "":
620+
Description "Your session is about to expire!":
621+
<div
622+
aria-describedby="some-id"
623+
role="dialog"
624+
/>
625+
626+
--------------------------------------------------
627+
button:
628+
629+
Name "Close":
630+
Description "":
631+
<button />
632+
633+
--------------------------------------------------
634+
635+
Ignored nodes: comments, <script />, <style />
636+
<body>
637+
<div
638+
aria-describedby="some-id"
639+
role="dialog"
640+
>
641+
<div>
642+
<button>
643+
Close
644+
</button>
645+
</div>
646+
<div
647+
id="some-id"
648+
>
649+
Your session is about to expire!
650+
</div>
651+
</div>
652+
</body>
653+
`)
654+
})
655+
656+
test('TextMatch serialization for description filter in error message', () => {
657+
const {getByRole} = renderIntoDocument(
658+
`<div role="alertdialog" aria-describedby="some-id"><div><button>Close</button></div><div id="some-id">Your session is about to expire!</div></div>`,
659+
)
660+
661+
expect(() => getByRole('alertdialog', {description: /unknown description/}))
662+
.toThrowErrorMatchingInlineSnapshot(`
663+
Unable to find an accessible element with the role "alertdialog" and description \`/unknown description/\`
664+
665+
Here are the accessible roles:
666+
667+
alertdialog:
668+
669+
Name "":
670+
Description "Your session is about to expire!":
671+
<div
672+
aria-describedby="some-id"
673+
role="alertdialog"
674+
/>
675+
676+
--------------------------------------------------
677+
button:
678+
679+
Name "Close":
680+
Description "":
681+
<button />
682+
683+
--------------------------------------------------
684+
685+
Ignored nodes: comments, <script />, <style />
686+
<body>
687+
<div
688+
aria-describedby="some-id"
689+
role="alertdialog"
690+
>
691+
<div>
692+
<button>
693+
Close
694+
</button>
695+
</div>
696+
<div
697+
id="some-id"
698+
>
699+
Your session is about to expire!
700+
</div>
701+
</div>
702+
</body>
703+
`)
704+
705+
expect(() => getByRole('alertdialog', {description: () => false}))
706+
.toThrowErrorMatchingInlineSnapshot(`
707+
Unable to find an accessible element with the role "alertdialog" and description \`() => false\`
708+
709+
Here are the accessible roles:
710+
711+
alertdialog:
712+
713+
Name "":
714+
Description "Your session is about to expire!":
715+
<div
716+
aria-describedby="some-id"
717+
role="alertdialog"
718+
/>
719+
720+
--------------------------------------------------
721+
button:
722+
723+
Name "Close":
724+
Description "":
725+
<button />
726+
727+
--------------------------------------------------
728+
729+
Ignored nodes: comments, <script />, <style />
730+
<body>
731+
<div
732+
aria-describedby="some-id"
733+
role="alertdialog"
734+
>
735+
<div>
736+
<button>
737+
Close
738+
</button>
739+
</div>
740+
<div
741+
id="some-id"
742+
>
743+
Your session is about to expire!
744+
</div>
745+
</div>
746+
</body>
747+
`)
748+
})

src/queries/role.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import {computeAccessibleName} from 'dom-accessibility-api'
1+
import {
2+
computeAccessibleDescription,
3+
computeAccessibleName,
4+
} from 'dom-accessibility-api'
25
import {roles as allRoles, roleElements} from 'aria-query'
36
import {
47
computeAriaSelected,
@@ -30,6 +33,7 @@ function queryAllByRole(
3033
collapseWhitespace,
3134
hidden = getConfig().defaultHidden,
3235
name,
36+
description,
3337
trim,
3438
normalizer,
3539
queryFallbacks = false,
@@ -169,6 +173,22 @@ function queryAllByRole(
169173
text => text,
170174
)
171175
})
176+
.filter(element => {
177+
if (description === undefined) {
178+
// Don't care
179+
return true
180+
}
181+
182+
return matches(
183+
computeAccessibleDescription(element, {
184+
computedStyleSupportsPseudoElements:
185+
getConfig().computedStyleSupportsPseudoElements,
186+
}),
187+
element,
188+
description,
189+
text => text,
190+
)
191+
})
172192
.filter(element => {
173193
return hidden === false
174194
? isInaccessible(element, {
@@ -216,7 +236,7 @@ const getMultipleError = (c, role, {name} = {}) => {
216236
const getMissingError = (
217237
container,
218238
role,
219-
{hidden = getConfig().defaultHidden, name} = {},
239+
{hidden = getConfig().defaultHidden, name, description} = {},
220240
) => {
221241
if (getConfig()._disableExpensiveErrorDiagnostics) {
222242
return `Unable to find role="${role}"`
@@ -227,6 +247,7 @@ const getMissingError = (
227247
roles += prettyRoles(childElement, {
228248
hidden,
229249
includeName: name !== undefined,
250+
includeDescription: description !== undefined,
230251
})
231252
})
232253
let roleMessage
@@ -257,10 +278,19 @@ Here are the ${hidden === false ? 'accessible' : 'available'} roles:
257278
nameHint = ` and name \`${name}\``
258279
}
259280

281+
let descriptionHint = ''
282+
if (description === undefined) {
283+
descriptionHint = ''
284+
} else if (typeof description === 'string') {
285+
descriptionHint = ` and description "${description}"`
286+
} else {
287+
descriptionHint = ` and description \`${description}\``
288+
}
289+
260290
return `
261291
Unable to find an ${
262292
hidden === false ? 'accessible ' : ''
263-
}element with the role "${role}"${nameHint}
293+
}element with the role "${role}"${nameHint}${descriptionHint}
264294
265295
${roleMessage}`.trim()
266296
}

src/role-helpers.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {elementRoles} from 'aria-query'
2-
import {computeAccessibleName} from 'dom-accessibility-api'
2+
import {
3+
computeAccessibleDescription,
4+
computeAccessibleName,
5+
} from 'dom-accessibility-api'
36
import {prettyDOM} from './pretty-dom'
47
import {getConfig} from './config'
58

@@ -178,7 +181,7 @@ function getRoles(container, {hidden = false} = {}) {
178181
}, {})
179182
}
180183

181-
function prettyRoles(dom, {hidden}) {
184+
function prettyRoles(dom, {hidden, includeDescription}) {
182185
const roles = getRoles(dom, {hidden})
183186
// We prefer to skip generic role, we don't recommend it
184187
return Object.entries(roles)
@@ -191,7 +194,20 @@ function prettyRoles(dom, {hidden}) {
191194
computedStyleSupportsPseudoElements:
192195
getConfig().computedStyleSupportsPseudoElements,
193196
})}":\n`
197+
194198
const domString = prettyDOM(el.cloneNode(false))
199+
200+
if (includeDescription) {
201+
const descriptionString = `Description "${computeAccessibleDescription(
202+
el,
203+
{
204+
computedStyleSupportsPseudoElements:
205+
getConfig().computedStyleSupportsPseudoElements,
206+
},
207+
)}":\n`
208+
return `${nameString}${descriptionString}${domString}`
209+
}
210+
195211
return `${nameString}${domString}`
196212
})
197213
.join('\n\n')

types/queries.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ export interface ByRoleOptions extends MatcherOptions {
115115
| RegExp
116116
| string
117117
| ((accessibleName: string, element: Element) => boolean)
118+
/**
119+
* Only considers elements with the specified accessible description.
120+
*/
121+
description?:
122+
| RegExp
123+
| string
124+
| ((accessibleDescription: string, element: Element) => boolean)
118125
}
119126

120127
export type AllByRole<T extends HTMLElement = HTMLElement> = (

0 commit comments

Comments
 (0)