[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(browser): show aria tree on locator element error (#10257)

Co-authored-by: Codex <noreply@openai.com>

authored by

Hiroshi Ogawa
Codex
and committed by
GitHub
(May 8, 2026, 8:17 AM +0100) 04f04cdd 6f74e5e9

+239 -2
+49
docs/config/browser/locators.md
··· 27 27 const locator = page.getByText('Hello, World', { exact: true }) 28 28 await locator.click() 29 29 ``` 30 + 31 + ## browser.locators.errorFormat <Version>5.0.0</Version> {#browser-locators-errorformat} 32 + 33 + - **Type:** `'html' | 'aria' | 'all'` 34 + - **Default:** `'all'` 35 + 36 + Controls what Vitest prints when a locator cannot find an element. Vitest prints information for the DOM subtree where the locator search ran, or `document.body` for page-level locators. 37 + 38 + - `'html'` prints that DOM subtree as HTML using [`utils.prettyDOM`](/api/browser/context#prettydom). 39 + - `'aria'` prints that DOM subtree as an [ARIA snapshot](/guide/browser/aria-snapshots), which focuses on accessible roles, names, and state. 40 + - `'all'` prints the ARIA snapshot first, followed by the HTML output. 41 + 42 + ```ts 43 + import { defineConfig } from 'vitest/config' 44 + 45 + export default defineConfig({ 46 + test: { 47 + browser: { 48 + enabled: true, 49 + locators: { 50 + errorFormat: 'aria', 51 + }, 52 + }, 53 + }, 54 + }) 55 + ``` 56 + 57 + For example, `all` displays a following error: 58 + 59 + ```html 60 + VitestBrowserElementError: Cannot find element with locator: getByRole('button', { name: 'Save' }) 61 + 62 + ARIA tree: 63 + - main: 64 + - heading "Settings" [level=1] 65 + - button "Cancel" 66 + 67 + HTML: 68 + <body> 69 + <main> 70 + <h1> 71 + Settings 72 + </h1> 73 + <button> 74 + Cancel 75 + </button> 76 + </main> 77 + </body> 78 + ```
+19 -1
packages/browser/src/client/tester/context.ts
··· 573 573 } 574 574 575 575 function getElementError(selector: string | Locator, container: Element): Error { 576 - const error = new Error(`Cannot find element with locator: ${typeof selector === 'string' ? __INTERNAL._asLocator('javascript', selector) : selector.asLocator()}\n\n${prettyDOM(container)}`) 576 + const locator = typeof selector === 'string' ? __INTERNAL._asLocator('javascript', selector) : selector.asLocator() 577 + const formatted = formatDOM(container) 578 + const error = new Error(`Cannot find element with locator: ${locator}\n\n${formatted}`) 577 579 error.name = 'VitestBrowserElementError' 578 580 return error 581 + } 582 + 583 + function formatDOM(container: Element): string { 584 + const format = getBrowserState().config.browser.locators.errorFormat 585 + if (format === 'aria') { 586 + return `ARIA tree:\n${formatAriaTree(container)}` 587 + } 588 + if (format === 'all') { 589 + return `ARIA tree:\n${formatAriaTree(container)}\n\nHTML:\n${prettyDOM(container)}` 590 + } 591 + return prettyDOM(container) 592 + } 593 + 594 + function formatAriaTree(container: Element): string { 595 + const { generateAriaTree, renderAriaTree } = getBrowserState().aria 596 + return renderAriaTree(generateAriaTree(container)) 579 597 } 580 598 581 599 function configurePrettyDOM(options: StringifyOptions) {
+1
packages/vitest/src/node/cli/cli-config.ts
··· 445 445 exact: { 446 446 description: 'Should locators match the text exactly by default (default: `false`)', 447 447 }, 448 + errorFormat: null, 448 449 }, 449 450 transform(val) { 450 451 if (typeof val !== 'object' || val == null) {
+1
packages/vitest/src/node/config/resolveConfig.ts
··· 860 860 resolved.browser.locators ??= {} as any 861 861 resolved.browser.locators.testIdAttribute ??= 'data-testid' 862 862 resolved.browser.locators.exact ??= false 863 + resolved.browser.locators.errorFormat ??= 'all' 863 864 864 865 if (typeof resolved.browser.provider === 'string') { 865 866 const source = `@vitest/browser-${resolved.browser.provider}`
+1
packages/vitest/src/node/config/serializeConfig.ts
··· 119 119 locators: { 120 120 testIdAttribute: browser.locators.testIdAttribute, 121 121 exact: browser.locators.exact, 122 + errorFormat: browser.locators.errorFormat, 122 123 }, 123 124 providerOptions: provider?.name === 'playwright' 124 125 ? {
+1
packages/vitest/src/node/projects/resolveProjects.ts
··· 295 295 ? { 296 296 testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, 297 297 exact: locators.exact ?? currentConfig.locators.exact, 298 + errorFormat: locators.errorFormat ?? currentConfig.locators.errorFormat, 298 299 } 299 300 : project.config.browser.locators, 300 301 viewport: viewport ?? currentConfig.viewport,
+7
packages/vitest/src/node/types/browser.ts
··· 240 240 * @default false 241 241 */ 242 242 exact?: boolean 243 + /** 244 + * Format used for locator "Cannot find element" error details. 245 + * 246 + * @default 'all' 247 + */ 248 + errorFormat?: 'html' | 'aria' | 'all' 243 249 } 244 250 245 251 /** ··· 447 453 locators: { 448 454 testIdAttribute: string 449 455 exact: boolean 456 + errorFormat: 'html' | 'aria' | 'all' 450 457 } 451 458 trace: { 452 459 mode: BrowserTraceViewMode
+1
packages/vitest/src/runtime/config.ts
··· 109 109 locators: { 110 110 testIdAttribute: string 111 111 exact: boolean 112 + errorFormat: 'html' | 'aria' | 'all' 112 113 } 113 114 screenshotFailures: boolean 114 115 providerOptions: {
+15
test/browser/fixtures/locator-error-format/basic.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { page } from 'vitest/browser' 3 + 4 + test('not found', async () => { 5 + document.body.innerHTML = ` 6 + <main> 7 + <h1>Settings</h1> 8 + <button>Cancel</button> 9 + </main> 10 + ` 11 + // TODO: surfacing element eror via expect.element is racy since 12 + // new timeout behavior https://github.com/vitest-dev/vitest/pull/10233 13 + // await expect.element(page.getByRole('button', { name: 'Save' }), { timeout: 200 }).toBeVisible() 14 + await page.getByRole('button', { name: 'Save' }).findElement({ timeout: 200 }) 15 + })
+12
test/browser/fixtures/locator-error-format/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + import { instances, provider } from '../../settings' 3 + 4 + export default defineConfig({ 5 + test: { 6 + browser: { 7 + enabled: true, 8 + provider, 9 + instances, 10 + }, 11 + }, 12 + })
+131
test/browser/specs/locator-error-format.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { instances, runBrowserTests } from './utils' 3 + 4 + test('locator error format aria', async () => { 5 + const result = await runBrowserTests({ 6 + root: './fixtures/locator-error-format', 7 + browser: { 8 + locators: { 9 + errorFormat: 'aria', 10 + }, 11 + }, 12 + }) 13 + 14 + const trees = result.errorTree({ project: true }) 15 + for (const { browser } of instances) { 16 + const tree = trees[browser] 17 + expect.soft(tree, browser).toMatchInlineSnapshot(` 18 + { 19 + "basic.test.ts": { 20 + "not found": [ 21 + "Cannot find element with locator: getByRole('button', { name: 'Save' }) 22 + 23 + ARIA tree: 24 + - main: 25 + - heading "Settings" [level=1] 26 + - button "Cancel"", 27 + ], 28 + }, 29 + } 30 + `) 31 + } 32 + }) 33 + 34 + test('locator error format html', async () => { 35 + const result = await runBrowserTests({ 36 + root: './fixtures/locator-error-format', 37 + browser: { 38 + locators: { 39 + errorFormat: 'html', 40 + }, 41 + }, 42 + }) 43 + 44 + const trees = result.errorTree({ project: true }) 45 + for (const { browser } of instances) { 46 + const tree = trees[browser] 47 + expect.soft(tree, browser).toMatchInlineSnapshot(` 48 + { 49 + "basic.test.ts": { 50 + "not found": [ 51 + "Cannot find element with locator: getByRole('button', { name: 'Save' }) 52 + 53 + <body> 54 + 55 + 56 + <main> 57 + 58 + 59 + <h1> 60 + Settings 61 + </h1> 62 + 63 + 64 + <button> 65 + Cancel 66 + </button> 67 + 68 + 69 + </main> 70 + 71 + 72 + </body>", 73 + ], 74 + }, 75 + } 76 + `) 77 + } 78 + }) 79 + 80 + test('locator error format all', async () => { 81 + const result = await runBrowserTests({ 82 + root: './fixtures/locator-error-format', 83 + browser: { 84 + locators: { 85 + // default 86 + // errorFormat: 'all', 87 + }, 88 + }, 89 + }) 90 + 91 + const trees = result.errorTree({ project: true }) 92 + for (const { browser } of instances) { 93 + const tree = trees[browser] 94 + expect.soft(tree, browser).toMatchInlineSnapshot(` 95 + { 96 + "basic.test.ts": { 97 + "not found": [ 98 + "Cannot find element with locator: getByRole('button', { name: 'Save' }) 99 + 100 + ARIA tree: 101 + - main: 102 + - heading "Settings" [level=1] 103 + - button "Cancel" 104 + 105 + HTML: 106 + <body> 107 + 108 + 109 + <main> 110 + 111 + 112 + <h1> 113 + Settings 114 + </h1> 115 + 116 + 117 + <button> 118 + Cancel 119 + </button> 120 + 121 + 122 + </main> 123 + 124 + 125 + </body>", 126 + ], 127 + }, 128 + } 129 + `) 130 + } 131 + })
+1 -1
test/test-utils/index.ts
··· 580 580 const root = testModules[0]?.project.config.root 581 581 582 582 function mapError(e: { message: string; diff?: string; stacks?: ParsedStack[] }) { 583 - let message = e.message 583 + let message = stripVTControlCharacters(e.message) 584 584 if (options?.diff && e.diff) { 585 585 message = [message, stripVTControlCharacters(e.diff)].join('\n') 586 586 }