Skip to content

Commit 5fa4103

Browse files
authored
Add heading levels to getAccessibilityTree (#445)
1 parent 1eaa648 commit 5fa4103

4 files changed

Lines changed: 120 additions & 41 deletions

File tree

.changeset/silent-candles-sleep.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'pleasantest': major
3+
---
4+
5+
Add heading levels to `getAccessibilityTree`. The heading levels are computed from the corresponding element number in `<h1>` - `<h6>`, or from the `aria-level` role.
6+
7+
In the accessibility tree snapshot, it looks like this:
8+
9+
```
10+
heading "Name of Heading" (level=2)
11+
```
12+
13+
This is a breaking change because it will cause existing accessibility tree snapshots to fail which contain headings. Update the snapshots to make them pass again.

examples/menu/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ test(
7979
expect(await getAccessibilityTree(await screen.getByRole('navigation')))
8080
.toMatchInlineSnapshot(`
8181
navigation
82-
heading "Company"
82+
heading "Company" (level=1)
8383
link "Company"
8484
text "Company"
8585
list
@@ -104,7 +104,7 @@ test(
104104
expect(await getAccessibilityTree(await screen.getByRole('navigation')))
105105
.toMatchInlineSnapshot(`
106106
navigation
107-
heading "Company"
107+
heading "Company" (level=1)
108108
link "Company"
109109
text "Company"
110110
list

src/accessibility/browser.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ export const getAccessibilityTree = (
124124
)
125125
text += ` (expanded=false)`;
126126
if (document.activeElement === element) text += ` (focused)`;
127+
if (role === 'heading') {
128+
const level =
129+
element.ariaLevel ||
130+
(element.tagName.length === 2 &&
131+
element.tagName.startsWith('H') &&
132+
element.tagName[1]);
133+
if (level) {
134+
text +=
135+
Number.parseInt(level, 10).toString() === level &&
136+
Number.parseInt(level, 10) > 0
137+
? ` (level=${level})`
138+
: ` (INVALID HEADING LEVEL: ${JSON.stringify(level)})`;
139+
} else {
140+
text += ` (MISSING HEADING LEVEL)`;
141+
}
142+
}
127143
if (includeDescriptions) {
128144
const description = computeAccessibleDescription(element);
129145
if (description) text += `\n ↳ description: "${description}"`;

tests/accessibility/getAccessibilityTree.test.ts

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ test(
4242
document "pleasantest"
4343
main
4444
button "Add to cart"
45-
heading "hiiii"
45+
heading "hiiii" (level=1)
4646
text "hiiii"
4747
button "foo > bar"
4848
`);
@@ -62,13 +62,13 @@ test(
6262
`);
6363
expect(await getAccessibilityTree(page, { includeText: true }))
6464
.toMatchInlineSnapshot(`
65-
document "pleasantest"
66-
list
67-
listitem
68-
text "something"
69-
listitem
70-
text "something else"
71-
`);
65+
document "pleasantest"
66+
list
67+
listitem
68+
text "something"
69+
listitem
70+
text "something else"
71+
`);
7272
await utils.injectHTML(`
7373
<button aria-describedby="click-me-description">click me</button>
7474
<button aria-describedby="click-me-description"><div>click me</div></button>
@@ -87,24 +87,24 @@ test(
8787
`);
8888
expect(await getAccessibilityTree(page, { includeText: true }))
8989
.toMatchInlineSnapshot(`
90-
document "pleasantest"
91-
button "click me"
92-
↳ description: "extended description"
93-
button "click me"
94-
↳ description: "extended description"
95-
button "click me"
96-
↳ description: "extended description"
97-
text "extended description"
98-
`);
90+
document "pleasantest"
91+
button "click me"
92+
↳ description: "extended description"
93+
button "click me"
94+
↳ description: "extended description"
95+
button "click me"
96+
↳ description: "extended description"
97+
text "extended description"
98+
`);
9999

100100
expect(await getAccessibilityTree(page, { includeDescriptions: false }))
101101
.toMatchInlineSnapshot(`
102-
document "pleasantest"
103-
button "click me"
104-
button "click me"
105-
button "click me"
106-
text "extended description"
107-
`);
102+
document "pleasantest"
103+
button "click me"
104+
button "click me"
105+
button "click me"
106+
text "extended description"
107+
`);
108108

109109
await utils.injectHTML(`
110110
<label>
@@ -118,12 +118,12 @@ test(
118118

119119
expect(await getAccessibilityTree(page, { includeText: true }))
120120
.toMatchInlineSnapshot(`
121-
document "pleasantest"
122-
text "Label Text"
123-
textbox "Label Text"
124-
text "Label Text"
125-
textbox "Label Text"
126-
`);
121+
document "pleasantest"
122+
text "Label Text"
123+
textbox "Label Text"
124+
text "Label Text"
125+
textbox "Label Text"
126+
`);
127127
}),
128128
);
129129

@@ -200,7 +200,7 @@ test(
200200
document "pleasantest"
201201
text "Sample Content"
202202
text "More Sample Content"
203-
heading "Hi"
203+
heading "Hi" (MISSING HEADING LEVEL)
204204
text "Hi"
205205
`);
206206
// Now the third list item has an explicit role which is the same as its implicit role.
@@ -235,7 +235,7 @@ test(
235235
document "pleasantest"
236236
text "Sample Content"
237237
text "More Sample Content"
238-
heading "Hi"
238+
heading "Hi" (MISSING HEADING LEVEL)
239239
text "Hi"
240240
`);
241241
// The required owned elements search should _not_ pass through elements with roles
@@ -250,11 +250,11 @@ test(
250250
`);
251251
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
252252
document "pleasantest"
253-
heading "Sample Content"
253+
heading "Sample Content" (level=1)
254254
listitem
255255
text "Sample Content"
256256
text "More Sample Content"
257-
heading "Hi"
257+
heading "Hi" (MISSING HEADING LEVEL)
258258
text "Hi"
259259
`);
260260
}),
@@ -281,6 +281,56 @@ test(
281281
}),
282282
);
283283

284+
test(
285+
'heading level labels',
286+
withBrowser(async ({ utils, page }) => {
287+
await utils.injectHTML(`
288+
<h1>Heading 1</h1>
289+
<h2>Heading 2</h2>
290+
<h3>Heading 3</h3>
291+
<h4>Heading 4</h4>
292+
<h5>Heading 5</h5>
293+
<h6>Heading 6</h6>
294+
295+
<div>Not a heading</div>
296+
<div aria-level="3">Not a heading</div>
297+
<div role="heading" aria-level="3">Heading 3 div</div>
298+
<div role="heading">Heading missing level</div>
299+
<div role="heading" aria-level="-2">Invalid heading level</div>
300+
<div role="heading" aria-level="asdf">Invalid heading level</div>
301+
<h2 aria-level="3">Heading 3 h2</h2>
302+
`);
303+
304+
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
305+
document "pleasantest"
306+
heading "Heading 1" (level=1)
307+
text "Heading 1"
308+
heading "Heading 2" (level=2)
309+
text "Heading 2"
310+
heading "Heading 3" (level=3)
311+
text "Heading 3"
312+
heading "Heading 4" (level=4)
313+
text "Heading 4"
314+
heading "Heading 5" (level=5)
315+
text "Heading 5"
316+
heading "Heading 6" (level=6)
317+
text "Heading 6"
318+
text "Not a heading"
319+
text "Not a heading"
320+
heading "Heading 3 div" (level=3)
321+
text "Heading 3 div"
322+
heading "Heading missing level" (MISSING HEADING LEVEL)
323+
text "Heading missing level"
324+
heading "Invalid heading level" (INVALID HEADING LEVEL: "-2")
325+
text "Invalid heading level"
326+
heading "Invalid heading level" (INVALID HEADING LEVEL: "asdf")
327+
text "Invalid heading level"
328+
heading "Heading 3 h2" (level=3)
329+
text "Heading 3 h2"
330+
`);
331+
}),
332+
);
333+
284334
test(
285335
'<details>/<summary>',
286336
withBrowser(async ({ utils, page, screen, user }) => {
@@ -297,7 +347,7 @@ test(
297347

298348
// Starts collapsed
299349
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
300-
document
350+
document "pleasantest"
301351
group
302352
button "Click me! Tags in summary do not preserve their semantic meaning" (expanded=false)
303353
`);
@@ -307,11 +357,11 @@ test(
307357

308358
// After toggling it should be expanded
309359
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
310-
document
360+
document "pleasantest"
311361
group
312362
button "Click me! Tags in summary do not preserve their semantic meaning" (expanded=true) (focused)
313363
text "Some content"
314-
heading "Tags in details do preserve their semantic meaning"
364+
heading "Tags in details do preserve their semantic meaning" (level=2)
315365
text "Tags in details do preserve their semantic meaning"
316366
`);
317367
}),
@@ -324,23 +374,23 @@ test(
324374
<button aria-expanded="false">Click me!</button>
325375
`);
326376
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
327-
document
377+
document "pleasantest"
328378
button "Click me!" (expanded=false)
329379
`);
330380

331381
await utils.injectHTML(`
332382
<button aria-expanded="true">Click me!</button>
333383
`);
334384
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
335-
document
385+
document "pleasantest"
336386
button "Click me!" (expanded=true)
337387
`);
338388

339389
await utils.injectHTML(`
340390
<button>Click me!</button>
341391
`);
342392
expect(await getAccessibilityTree(page)).toMatchInlineSnapshot(`
343-
document
393+
document "pleasantest"
344394
button "Click me!"
345395
`);
346396
}),

0 commit comments

Comments
 (0)