Skip to content

Commit c342301

Browse files
authored
feat(browser): export aria tree utils (#10171)
1 parent 72a6dc2 commit c342301

11 files changed

Lines changed: 114 additions & 3 deletions

File tree

‎docs/api/browser/context.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,17 @@ export const utils: {
270270
* Creates "Cannot find element" error. Useful for custom locators.
271271
*/
272272
getElementError(selector: string, container?: Element): Error
273+
/**
274+
* Utilities for generating and working with ARIA trees and templates.
275+
* @experimental
276+
*/
277+
aria: {
278+
generateAriaTree(rootElement: Element): AriaNode
279+
renderAriaTree(root: AriaNode): string
280+
renderAriaTemplate(template: AriaTemplateNode): string
281+
parseAriaTemplate(text: string): AriaTemplateNode
282+
matchAriaTree(root: AriaNode, template: AriaTemplateNode): { pass: boolean; resolved: string }
283+
}
273284
}
274285
```
275286

@@ -340,3 +351,22 @@ utils.configurePrettyDOM({
340351
::: tip
341352
This feature is inspired by Testing Library's [`defaultIgnore`](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaultignore) configuration.
342353
:::
354+
355+
### aria <Version type="experimental">5.0.0</Version> {#aria}
356+
357+
The `aria` namespace exposes low-level utilities used by Vitest's ARIA snapshot matchers.
358+
359+
```ts
360+
import { utils } from 'vitest/browser'
361+
362+
document.body.innerHTML = `
363+
<h1>Hello, World!</h1>
364+
<button aria-hidden="true">Hidden</button>
365+
<button>Visible</button>
366+
`
367+
const tree = utils.aria.generateAriaTree(document.body)
368+
const yaml = utils.aria.renderAriaNode(tree)
369+
console.log(yaml)
370+
// - heading "Hello, World!" [level=1]
371+
// - button "Visible""
372+
```

‎docs/guide/browser/aria-snapshots.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(`
3030

3131
This catches accessibility regressions: missing labels, broken roles, incorrect heading levels, and more — things that DOM snapshots would miss. Even if the underlying HTML structure changes, the assertion would not fail as long as content matches semantically.
3232

33+
For advanced cases, you can also generate and inspect the ARIA tree through `utils.aria` from `vitest/browser`. See the [Context API](/api/browser/context#aria) for details.
34+
3335
## Snapshot Workflow
3436

3537
ARIA snapshots use the same Vitest snapshot workflow as other snapshot assertions. File snapshots, inline snapshots, `--update` / `-u`, watch mode updates, and CI snapshot behavior all work the same way.

‎packages/browser/context.d.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SerializedConfig } from 'vitest'
22
import { StringifyOptions, CDPSession, BrowserCommands } from 'vitest/internal/browser'
33
import { ARIARole } from './aria-role.js'
44
import {} from './matchers.js'
5+
import { __ivyaAriaTypes } from '@vitest/browser/internal/vendor-types'
56

67
export type BufferEncoding =
78
| 'ascii'
@@ -934,6 +935,23 @@ export const utils: {
934935
* Creates "Cannot find element" error. Useful for custom locators.
935936
*/
936937
getElementError(selector: string, container?: Element): Error
938+
939+
/**
940+
* Utilities for generating and working with ARIA trees and templates.
941+
* @experimental
942+
*/
943+
aria: {
944+
/** Captures the ARIA tree for a DOM subtree. */
945+
generateAriaTree: typeof __ivyaAriaTypes.generateAriaTree
946+
/** Renders a captured ARIA tree to the textual snapshot format. */
947+
renderAriaTree: typeof __ivyaAriaTypes.renderAriaTree
948+
/** Renders an ARIA template back to text. */
949+
renderAriaTemplate: typeof __ivyaAriaTypes.renderAriaTemplate
950+
/** Parses textual ARIA snapshot syntax into a template tree. */
951+
parseAriaTemplate: typeof __ivyaAriaTypes.parseAriaTemplate
952+
/** Matches a captured ARIA tree against a parsed template. */
953+
matchAriaTree: typeof __ivyaAriaTypes.matchAriaTree
954+
}
937955
}
938956

939957
export const locators: BrowserLocators

‎packages/browser/package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
"./utils": {
4545
"default": "./dummy.js"
4646
},
47+
"./internal/vendor-types": {
48+
"types": "./dist/vendor-types.d.ts",
49+
"default": "./dummy.js"
50+
},
4751
"./package.json": "./package.json"
4852
},
4953
"main": "./dist/index.js",

‎packages/browser/rollup.config.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,36 @@ export default () =>
157157
external,
158158
plugins: dtsUtilsClient.dts(),
159159
},
160+
{
161+
input: {
162+
'vendor-types': './src/vendor-types.ts',
163+
},
164+
output: {
165+
dir: 'dist',
166+
entryFileNames: '[name].ts',
167+
format: 'esm',
168+
},
169+
external,
170+
plugins: [
171+
...dtsUtils.isolatedDecl(),
172+
...plugins,
173+
],
174+
},
175+
{
176+
input: {
177+
'vendor-types': './dist/.types/vendor-types.d.ts',
178+
},
179+
output: {
180+
dir: 'dist',
181+
entryFileNames: '[name].d.ts',
182+
format: 'esm',
183+
},
184+
external,
185+
plugins: [
186+
resolve({
187+
preferBuiltins: true,
188+
}),
189+
dtsUtils.dts(),
190+
],
191+
},
160192
])

‎packages/browser/src/client/tester/aria.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import type {
44
AriaNode,
55
AriaTemplateNode,
66
} from 'ivya/aria'
7-
import {
7+
import * as aria from 'ivya/aria'
8+
import { Snapshots } from 'vitest'
9+
import { getBrowserState } from '../utils'
10+
11+
getBrowserState().aria = aria
12+
13+
const {
814
generateAriaTree,
915
matchAriaTree,
1016
parseAriaTemplate,
1117
renderAriaTemplate,
1218
renderAriaTree,
13-
} from 'ivya/aria'
14-
import { Snapshots } from 'vitest'
19+
} = aria
1520

1621
const ariaSnapshotAdapter: DomainSnapshotAdapter<AriaNode, AriaTemplateNode> = {
1722
name: 'aria',

‎packages/browser/src/client/tester/context.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,7 @@ export const utils = {
556556
debug,
557557
getElementLocatorSelectors,
558558
configurePrettyDOM,
559+
get aria() {
560+
return getBrowserState().aria
561+
},
559562
}

‎packages/browser/src/client/utils.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface BrowserRunnerState {
9595
send: (method: string, params?: Record<string, unknown>) => Promise<unknown>
9696
emit: (event: string, payload: unknown) => void
9797
}
98+
aria: typeof import('ivya/aria')
9899
}
99100

100101
/* @__NO_SIDE_EFFECTS__ */
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type * as __ivyaAriaTypes from 'ivya/aria'

‎test/browser/test/utils.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,17 @@ test('filterNode with wildcard selector filters nested content', async () => {
206206
</div>"
207207
`)
208208
})
209+
210+
test('aria tree utils', () => {
211+
document.body.innerHTML = `
212+
<h1>Hello, World!</h1>
213+
<button aria-hidden="true">Hidden</button>
214+
<button>Visible</button>
215+
`
216+
const { generateAriaTree, renderAriaTree } = utils.aria
217+
expect(`\n${renderAriaTree(generateAriaTree(document.body))}`).toMatchInlineSnapshot(`
218+
"
219+
- heading "Hello, World!" [level=1]
220+
- button "Visible""
221+
`)
222+
})

0 commit comments

Comments
 (0)