Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/api/browser/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ export const utils: {
* Creates "Cannot find element" error. Useful for custom locators.
*/
getElementError(selector: string, container?: Element): Error
/**
* Utilities for generating and working with ARIA trees and templates.
* @experimental
*/
aria: {
generateAriaTree(rootElement: Element): AriaNode
renderAriaTree(root: AriaNode): string
renderAriaTemplate(template: AriaTemplateNode): string
parseAriaTemplate(text: string): AriaTemplateNode
matchAriaTree(root: AriaNode, template: AriaTemplateNode): { pass: boolean; resolved: string }
}
}
```

Expand Down Expand Up @@ -340,3 +351,22 @@ utils.configurePrettyDOM({
::: tip
This feature is inspired by Testing Library's [`defaultIgnore`](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaultignore) configuration.
:::

### aria <Version type="experimental">5.0.0</Version> {#aria}

The `aria` namespace exposes low-level utilities used by Vitest's ARIA snapshot matchers.

```ts
import { utils } from 'vitest/browser'

document.body.innerHTML = `
<h1>Hello, World!</h1>
<button aria-hidden="true">Hidden</button>
<button>Visible</button>
`
const tree = utils.aria.generateAriaTree(document.body)
const yaml = utils.aria.renderAriaNode(tree)
console.log(yaml)
// - heading "Hello, World!" [level=1]
// - button "Visible""
```
2 changes: 2 additions & 0 deletions docs/guide/browser/aria-snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(`

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.

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.

## Snapshot Workflow

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.
Expand Down
18 changes: 18 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SerializedConfig } from 'vitest'
import { StringifyOptions, CDPSession, BrowserCommands } from 'vitest/internal/browser'
import { ARIARole } from './aria-role.js'
import {} from './matchers.js'
import { __ivyaAriaTypes } from '@vitest/browser/internal/vendor-types'

export type BufferEncoding =
| 'ascii'
Expand Down Expand Up @@ -934,6 +935,23 @@ export const utils: {
* Creates "Cannot find element" error. Useful for custom locators.
*/
getElementError(selector: string, container?: Element): Error

/**
* Utilities for generating and working with ARIA trees and templates.
* @experimental
*/
aria: {
/** Captures the ARIA tree for a DOM subtree. */
generateAriaTree: typeof __ivyaAriaTypes.generateAriaTree
/** Renders a captured ARIA tree to the textual snapshot format. */
renderAriaTree: typeof __ivyaAriaTypes.renderAriaTree
/** Renders an ARIA template back to text. */
renderAriaTemplate: typeof __ivyaAriaTypes.renderAriaTemplate
/** Parses textual ARIA snapshot syntax into a template tree. */
parseAriaTemplate: typeof __ivyaAriaTypes.parseAriaTemplate
/** Matches a captured ARIA tree against a parsed template. */
matchAriaTree: typeof __ivyaAriaTypes.matchAriaTree
}
Comment on lines +939 to +954
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript couldn't infer jsdoc from the original ivya/aria exports (likely due to dts bundling), so I wrote up it manually by partially duplicating the ones in ivya.

}

export const locators: BrowserLocators
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
"./utils": {
"default": "./dummy.js"
},
"./internal/vendor-types": {
"types": "./dist/vendor-types.d.ts",
"default": "./dummy.js"
},
"./package.json": "./package.json"
},
"main": "./dist/index.js",
Expand Down
32 changes: 32 additions & 0 deletions packages/browser/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,36 @@ export default () =>
external,
plugins: dtsUtilsClient.dts(),
},
{
input: {
'vendor-types': './src/vendor-types.ts',
},
output: {
dir: 'dist',
entryFileNames: '[name].ts',
format: 'esm',
},
external,
plugins: [
...dtsUtils.isolatedDecl(),
...plugins,
],
},
{
input: {
'vendor-types': './dist/.types/vendor-types.d.ts',
},
output: {
dir: 'dist',
entryFileNames: '[name].d.ts',
format: 'esm',
},
external,
plugins: [
resolve({
preferBuiltins: true,
}),
dtsUtils.dts(),
],
},
])
11 changes: 8 additions & 3 deletions packages/browser/src/client/tester/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import type {
AriaNode,
AriaTemplateNode,
} from 'ivya/aria'
import {
import * as aria from 'ivya/aria'
import { Snapshots } from 'vitest'
import { getBrowserState } from '../utils'

getBrowserState().aria = aria

const {
generateAriaTree,
matchAriaTree,
parseAriaTemplate,
renderAriaTemplate,
renderAriaTree,
} from 'ivya/aria'
import { Snapshots } from 'vitest'
} = aria

const ariaSnapshotAdapter: DomainSnapshotAdapter<AriaNode, AriaTemplateNode> = {
name: 'aria',
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,7 @@ export const utils = {
debug,
getElementLocatorSelectors,
configurePrettyDOM,
get aria() {
return getBrowserState().aria
},
Comment on lines +559 to +561
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly does this work-around fix? Why not import { aria } from "./aria" and return that here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some weird @vitest/browser client bundle split and we have tester/context.ts and tester/expect-element.ts independently built without sharing chunks. Previously ivya/aria lived only in expect-element bundle, but this one now needs to be available from context.ts, so it's passed through __INTERNAL global.

There's other globals that are passed around like __vitest_browser_runner__, but haven't fully understood what is for what.

}
1 change: 1 addition & 0 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface BrowserRunnerState {
send: (method: string, params?: Record<string, unknown>) => Promise<unknown>
emit: (event: string, payload: unknown) => void
}
aria: typeof import('ivya/aria')
}

/* @__NO_SIDE_EFFECTS__ */
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/vendor-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * as __ivyaAriaTypes from 'ivya/aria'
14 changes: 14 additions & 0 deletions test/browser/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,17 @@ test('filterNode with wildcard selector filters nested content', async () => {
</div>"
`)
})

test('aria tree utils', () => {
document.body.innerHTML = `
<h1>Hello, World!</h1>
<button aria-hidden="true">Hidden</button>
<button>Visible</button>
`
const { generateAriaTree, renderAriaTree } = utils.aria
expect(`\n${renderAriaTree(generateAriaTree(document.body))}`).toMatchInlineSnapshot(`
"
- heading "Hello, World!" [level=1]
- button "Visible""
`)
})
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@vitest/browser-playwright": ["./packages/browser-playwright/src/index.ts"],
"@vitest/browser": ["./packages/browser/src/node/index.ts"],
"@vitest/browser/client": ["./packages/browser/src/client/client.ts"],
"@vitest/browser/internal/vendor-types": ["./packages/browser/src/vendor-types.ts"],
"~/*": ["./packages/ui/client/*"],
"vitest": ["./packages/vitest/src/public/index.ts"],
"vitest/internal/browser": ["./packages/vitest/src/public/browser.ts"],
Expand Down
Loading