From c3423014c1dc69092dff55dd6d82c61e4402fe96 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 23 Apr 2026 18:03:45 +0900 Subject: [PATCH] feat(browser): export aria tree utils (#10171) --- docs/api/browser/context.md | 30 +++++++++++++++++ docs/guide/browser/aria-snapshots.md | 2 ++ packages/browser/context.d.ts | 18 +++++++++++ packages/browser/package.json | 4 +++ packages/browser/rollup.config.js | 32 +++++++++++++++++++ packages/browser/src/client/tester/aria.ts | 11 +++++-- packages/browser/src/client/tester/context.ts | 3 ++ packages/browser/src/client/utils.ts | 1 + packages/browser/src/vendor-types.ts | 1 + test/browser/test/utils.test.ts | 14 ++++++++ tsconfig.base.json | 1 + 11 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 packages/browser/src/vendor-types.ts diff --git a/docs/api/browser/context.md b/docs/api/browser/context.md index c4eee0d25..49f75e4f6 100644 --- a/docs/api/browser/context.md +++ b/docs/api/browser/context.md @@ -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 } + } } ``` @@ -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 5.0.0 {#aria} + +The `aria` namespace exposes low-level utilities used by Vitest's ARIA snapshot matchers. + +```ts +import { utils } from 'vitest/browser' + +document.body.innerHTML = ` +

Hello, World!

+ + +` +const tree = utils.aria.generateAriaTree(document.body) +const yaml = utils.aria.renderAriaNode(tree) +console.log(yaml) +// - heading "Hello, World!" [level=1] +// - button "Visible"" +``` diff --git a/docs/guide/browser/aria-snapshots.md b/docs/guide/browser/aria-snapshots.md index d4dee8257..d7d8ae76f 100644 --- a/docs/guide/browser/aria-snapshots.md +++ b/docs/guide/browser/aria-snapshots.md @@ -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. diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index d9a2f8e76..f99c32e27 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -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' @@ -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 + } } export const locators: BrowserLocators diff --git a/packages/browser/package.json b/packages/browser/package.json index 7dd27fbaf..75475f997 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -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", diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index b32c1783e..b7ed904c7 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -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(), + ], + }, ]) diff --git a/packages/browser/src/client/tester/aria.ts b/packages/browser/src/client/tester/aria.ts index 98d5dd143..67335c05a 100644 --- a/packages/browser/src/client/tester/aria.ts +++ b/packages/browser/src/client/tester/aria.ts @@ -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 = { name: 'aria', diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 5d230a880..8bfedcd8c 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -556,4 +556,7 @@ export const utils = { debug, getElementLocatorSelectors, configurePrettyDOM, + get aria() { + return getBrowserState().aria + }, } diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 2091f614f..4693ad98d 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -95,6 +95,7 @@ export interface BrowserRunnerState { send: (method: string, params?: Record) => Promise emit: (event: string, payload: unknown) => void } + aria: typeof import('ivya/aria') } /* @__NO_SIDE_EFFECTS__ */ diff --git a/packages/browser/src/vendor-types.ts b/packages/browser/src/vendor-types.ts new file mode 100644 index 000000000..3a161be89 --- /dev/null +++ b/packages/browser/src/vendor-types.ts @@ -0,0 +1 @@ +export type * as __ivyaAriaTypes from 'ivya/aria' diff --git a/test/browser/test/utils.test.ts b/test/browser/test/utils.test.ts index b4099511d..d1b8e7299 100644 --- a/test/browser/test/utils.test.ts +++ b/test/browser/test/utils.test.ts @@ -206,3 +206,17 @@ test('filterNode with wildcard selector filters nested content', async () => { " `) }) + +test('aria tree utils', () => { + document.body.innerHTML = ` +

Hello, World!

+ + + ` + const { generateAriaTree, renderAriaTree } = utils.aria + expect(`\n${renderAriaTree(generateAriaTree(document.body))}`).toMatchInlineSnapshot(` + " + - heading "Hello, World!" [level=1] + - button "Visible"" + `) +}) diff --git a/tsconfig.base.json b/tsconfig.base.json index 41c24c2a7..4b932dc6d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"], -- 2.51.2