Skip to content

Commit 04f04cd

Browse files
hi-ogawacodex
andauthored
feat(browser): show aria tree on locator element error (#10257)
Co-authored-by: Codex <[email protected]>
1 parent 6f74e5e commit 04f04cd

12 files changed

Lines changed: 239 additions & 2 deletions

File tree

‎docs/config/browser/locators.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,52 @@ When set to `true`, [locators](/api/browser/locators) will match text exactly by
2727
const locator = page.getByText('Hello, World', { exact: true })
2828
await locator.click()
2929
```
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+
```

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,11 +573,29 @@ function prettyDOM(
573573
}
574574

575575
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}`)
577579
error.name = 'VitestBrowserElementError'
578580
return error
579581
}
580582

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))
597+
}
598+
581599
function configurePrettyDOM(options: StringifyOptions) {
582600
defaultOptions = options
583601
}

‎packages/vitest/src/node/cli/cli-config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
445445
exact: {
446446
description: 'Should locators match the text exactly by default (default: `false`)',
447447
},
448+
errorFormat: null,
448449
},
449450
transform(val) {
450451
if (typeof val !== 'object' || val == null) {

‎packages/vitest/src/node/config/resolveConfig.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@ export function resolveConfig(
860860
resolved.browser.locators ??= {} as any
861861
resolved.browser.locators.testIdAttribute ??= 'data-testid'
862862
resolved.browser.locators.exact ??= false
863+
resolved.browser.locators.errorFormat ??= 'all'
863864

864865
if (typeof resolved.browser.provider === 'string') {
865866
const source = `@vitest/browser-${resolved.browser.provider}`

‎packages/vitest/src/node/config/serializeConfig.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
119119
locators: {
120120
testIdAttribute: browser.locators.testIdAttribute,
121121
exact: browser.locators.exact,
122+
errorFormat: browser.locators.errorFormat,
122123
},
123124
providerOptions: provider?.name === 'playwright'
124125
? {

‎packages/vitest/src/node/projects/resolveProjects.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstan
295295
? {
296296
testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute,
297297
exact: locators.exact ?? currentConfig.locators.exact,
298+
errorFormat: locators.errorFormat ?? currentConfig.locators.errorFormat,
298299
}
299300
: project.config.browser.locators,
300301
viewport: viewport ?? currentConfig.viewport,

‎packages/vitest/src/node/types/browser.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ export interface BrowserConfigOptions {
240240
* @default false
241241
*/
242242
exact?: boolean
243+
/**
244+
* Format used for locator "Cannot find element" error details.
245+
*
246+
* @default 'all'
247+
*/
248+
errorFormat?: 'html' | 'aria' | 'all'
243249
}
244250

245251
/**
@@ -447,6 +453,7 @@ export interface ResolvedBrowserOptions extends BrowserConfigOptions {
447453
locators: {
448454
testIdAttribute: string
449455
exact: boolean
456+
errorFormat: 'html' | 'aria' | 'all'
450457
}
451458
trace: {
452459
mode: BrowserTraceViewMode

‎packages/vitest/src/runtime/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export interface SerializedConfig {
109109
locators: {
110110
testIdAttribute: string
111111
exact: boolean
112+
errorFormat: 'html' | 'aria' | 'all'
112113
}
113114
screenshotFailures: boolean
114115
providerOptions: {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
})

0 commit comments

Comments
 (0)