GeezerVison is showing github.com — feat(browser): capture aria snapshot in trace view (#11492) · vitest-dev/vitest@0cc39fb
File treeExpand file treeCollapse file tree Open diff view settings browser/src/client/tester
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: docs/config/browser/traceview.md +10-1Lines changed: 10 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,7 +5,7 @@ outline: deep | 5 | 5 |
| 6 | 6 | # browser.traceView <Version>5.0.0</Version> | 7 | 7 |
| 8 | | -- **Type:** `boolean | { enabled?: boolean; recordCanvas?: boolean; inlineImages?: boolean }` | | 8 | +- **Type:** `boolean | { enabled?: boolean; recordCanvas?: boolean; inlineImages?: boolean; ariaSnapshot?: boolean }` | 9 | 9 | - **CLI:** `--browser.traceView` | 10 | 10 | - **Default:** `false` | 11 | 11 |
| @@ -42,6 +42,7 @@ export default defineConfig({ | 42 | 42 | | `enabled` | `false` | Enables Vitest trace-view artifact collection. | | 43 | 43 | | `inlineImages` | `false` | Inlines loaded `<img>` pixels into snapshots for more portable replay, useful in the HTML reporter. | | 44 | 44 | | `recordCanvas` | `false` | Captures canvas pixels in snapshots. | | | 45 | +| `ariaSnapshot` | `true` | Captures the accessibility tree alongside each snapshot. | | 45 | 46 |
| 46 | 47 | ## browser.traceView.enabled {#traceview-enabled} | 47 | 48 |
| @@ -67,4 +68,12 @@ Inlines loaded `<img>` pixels into snapshots for more portable replay, useful in | 67 | 68 |
| 68 | 69 | Captures canvas pixels in snapshots. This enables a weaker replay iframe sandbox because rrweb needs scripts to redraw canvas data. | 69 | 70 |
| | 71 | +## browser.traceView.ariaSnapshot <Version>5.1.0</Version> {#traceview-ariasnapshot} | | 72 | + | | 73 | +- **Type:** `boolean` | | 74 | +- **Default:** `true` | | 75 | +- **CLI:** `--browser.traceView.ariaSnapshot` | | 76 | + | | 77 | +Captures the accessibility tree of the page alongside each snapshot. The trace viewer header can then switch from the DOM snapshot to an **Aria** view, which uses the same format as [aria snapshots](/guide/browser/aria-snapshots). | | 78 | + | 70 | 79 | See [Trace View](/guide/browser/trace-view) for full documentation. |
Collapse file Copy file name to clipboardExpand all lines: docs/guide/browser/trace-view.md +4Lines changed: 4 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -149,6 +149,10 @@ test('shows button', async () => { | 149 | 149 |
| 150 | 150 | Each attempt — retry or repeat — is recorded as a separate trace. When a test has multiple attempts, the viewer opens the most recent one by default. You can switch between attempts in the Report tab. | 151 | 151 |
| | 152 | +## Accessibility Tree <Version>5.1.0</Version> {#accessibility-tree} | | 153 | + | | 154 | +Trace view also captures the page's accessibility tree at each step. The trace viewer header shows a view selector to switch the snapshot pane between **DOM** and **Aria**. The Aria view lists the roles, accessible names, and states that role-based locators such as `getByRole()` see, in the same format as [aria snapshots](/guide/browser/aria-snapshots). Set [`traceView.ariaSnapshot`](/config/browser/traceview#traceview-ariasnapshot) to `false` to disable it. | | 155 | + | 152 | 156 | ## Snapshot Fidelity | 153 | 157 |
| 154 | 158 | By default, trace view captures the DOM tree, attributes, form values, same-origin readable CSS, element scroll positions, viewport size, and window scroll position. Images and canvas pixels are not inlined by default. | |
Collapse file Copy file name to clipboardExpand all lines: docs/guide/cli-generated.md +7Lines changed: 7 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -430,6 +430,13 @@ Capture canvas pixels in trace-view snapshots (default: `false`) | 430 | 430 |
| 431 | 431 | Inline loaded image pixels in trace-view snapshots (default: `false`) | 432 | 432 |
| | 433 | +### browser.traceView.ariaSnapshot | | 434 | + | | 435 | +- **CLI:** `--browser.traceView.ariaSnapshot` | | 436 | +- **Config:** [browser.traceView.ariaSnapshot](/config/browser/traceview#traceview-ariasnapshot) | | 437 | + | | 438 | +Capture the accessibility tree in trace-view snapshots (default: `true`) | | 439 | + | 433 | 440 | ### browser.locators.exact | 434 | 441 |
| 435 | 442 | - **CLI:** `--browser.locators.exact` | |
Collapse file Copy file name to clipboardExpand all lines: packages/browser/src/client/tester/trace.ts +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -53,6 +53,7 @@ interface TraceSnapshot { | 53 | 53 | selectorResolution?: BrowserTraceSelectorResolution | 54 | 54 | selectorError?: string | 55 | 55 | pseudoClassIds: Record<PseudoClassName, number[]> | | 56 | + ariaSnapshot?: string | 56 | 57 | } | 57 | 58 |
| 58 | 59 | // Dynamic pseudo-class state isn't preserved in the serialized DOM. rrweb-snapshot | @@ -147,6 +148,10 @@ function takeSnapshot(serializedLocator?: SerializedLocator): TraceSnapshot { | 147 | 148 | }, | 148 | 149 | pseudoClassIds: {} as any, | 149 | 150 | } | | 151 | + if (traceView.ariaSnapshot) { | | 152 | + const { generateAriaTree, renderAriaTree } = getBrowserState().aria | | 153 | + result.ariaSnapshot = renderAriaTree(generateAriaTree(document.body)) | | 154 | + } | 150 | 155 | for (const className of PSEUDO_CLASS_NAMES) { | 151 | 156 | const elements = document.querySelectorAll(className) | 152 | 157 | const ids = Array.from(elements, (el) => mirror.getId(el)).filter((id) => id !== -1) | |
Collapse file Copy file name to clipboardExpand all lines: packages/ui/client/components/trace/TraceView.vue +12-6Lines changed: 12 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,6 +14,7 @@ import { | 14 | 14 | getTraceEntryClass, | 15 | 15 | selectActiveTraceStep, | 16 | 16 | showTraceSelectorHighlight, | | 17 | + useTraceSnapshotViewMode, | 17 | 18 | useTraceZoom, | 18 | 19 | } from '~/composables/trace-view' | 19 | 20 |
| @@ -24,6 +25,7 @@ const props = defineProps<{ | 24 | 25 |
| 25 | 26 | const entries = computed(() => props.trace.entries) | 26 | 27 | const selectedStep = computed(() => entries.value[props.selection.selectedStepIndex]) | | 28 | +const { showAriaSnapshot } = useTraceSnapshotViewMode(() => props.trace) | 27 | 29 |
| 28 | 30 | const iframeEl = ref<HTMLIFrameElement>() | 29 | 31 | const iframeSandbox = computed(() => { | @@ -267,11 +269,16 @@ function onSplitpanesResized({ panes }: SplitpanesResizedPayload) { | 267 | 269 | </Pane> | 268 | 270 | <Pane :size="traceViewSplitSizes[1]" min-size="20"> | 269 | 271 | <div :ref="setFitContainer" class="h-full min-h-0 flex flex-col overflow-auto"> | 270 | | - <div | 271 | | - v-if="selectedStep" | 272 | | - class="mx-auto flex-none overflow-hidden" | 273 | | - :style="scaledViewportStyle" | 274 | | - > | | 272 | + <div v-if="!selectedStep" class="text-sm opacity-50 p-4">No trace step found</div> | | 273 | + <template v-else-if="showAriaSnapshot"> | | 274 | + <pre | | 275 | + v-if="selectedStep.snapshot.ariaSnapshot != null" | | 276 | + data-testid="trace-aria-snapshot" | | 277 | + class="m-0 p-4 font-mono text-xs whitespace-pre" | | 278 | + >{{ selectedStep.snapshot.ariaSnapshot }}</pre> | | 279 | + <div v-else class="text-sm opacity-50 p-4">No aria snapshot for this step</div> | | 280 | + </template> | | 281 | + <div v-else class="mx-auto flex-none overflow-hidden" :style="scaledViewportStyle"> | 275 | 282 | <iframe | 276 | 283 | ref="iframeEl" | 277 | 284 | :key="iframeSandbox" | @@ -280,7 +287,6 @@ function onSplitpanesResized({ panes }: SplitpanesResizedPayload) { | 280 | 287 | :style="{ transform: `scale(${zoomLevel})` }" | 281 | 288 | /> | 282 | 289 | </div> | 283 | | - <div v-else class="text-sm opacity-50 p-4">No trace step found</div> | 284 | 290 | </div> | 285 | 291 | </Pane> | 286 | 292 | </Splitpanes> | |
Collapse file Copy file name to clipboardExpand all lines: packages/ui/client/components/trace/TraceViewPane.vue +19-3Lines changed: 19 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,6 +10,7 @@ import { | 10 | 10 | getTraceAttemptMap, | 11 | 11 | selectActiveTraceAttempt, | 12 | 12 | showTraceSelectorHighlight, | | 13 | + useTraceSnapshotViewMode, | 13 | 14 | } from '~/composables/trace-view' | 14 | 15 | import { getNames } from '../../../../vitest/src/utils/tasks.ts' | 15 | 16 | import TraceView from './TraceView.vue' | @@ -21,6 +22,9 @@ const props = defineProps<{ | 21 | 22 |
| 22 | 23 | const trace = computed(() => getSelectedTrace(props.selection)) | 23 | 24 | const attemptLabel = computed(() => (trace.value ? getTraceAttemptLabel(trace.value) : '')) | | 25 | +const { snapshotViewMode, hasAriaSnapshot, showAriaSnapshot } = useTraceSnapshotViewMode( | | 26 | + () => trace.value, | | 27 | +) | 24 | 28 | const traceAttempts = computed(() => | 25 | 29 | [...getTraceAttemptMap(props.selection.test.artifacts)].map(([key, trace]) => ({ | 26 | 30 | key, | @@ -74,9 +78,21 @@ const traceLayoutPageUrl = computed(() => { | 74 | 78 | <span v-else-if="attemptLabel" class="text-xs opacity-70"> | 75 | 79 | {{ attemptLabel }} | 76 | 80 | </span> | 77 | | - <TraceZoomControl v-if="trace" /> | 78 | | - <label class="flex items-center gap-1 text-xs ws-nowrap select-none cursor-pointer"> | 79 | | - <input v-model="showTraceSelectorHighlight" type="checkbox" /> | | 81 | + <select | | 82 | + v-if="hasAriaSnapshot" | | 83 | + v-model="snapshotViewMode" | | 84 | + aria-label="Trace snapshot view" | | 85 | + class="cursor-pointer border border-base rounded bg-base px-2 py-1 text-xs" | | 86 | + > | | 87 | + <option value="dom">DOM</option> | | 88 | + <option value="aria">Aria</option> | | 89 | + </select> | | 90 | + <TraceZoomControl v-if="trace" :disabled="showAriaSnapshot" /> | | 91 | + <label | | 92 | + class="flex items-center gap-1 text-xs ws-nowrap select-none" | | 93 | + :class="showAriaSnapshot ? 'op-50 cursor-not-allowed' : 'cursor-pointer'" | | 94 | + > | | 95 | + <input v-model="showTraceSelectorHighlight" type="checkbox" :disabled="showAriaSnapshot" /> | 80 | 96 | <span>Show highlight</span> | 81 | 97 | </label> | 82 | 98 | <a | |
Collapse file Copy file name to clipboardExpand all lines: packages/ui/client/components/trace/TraceZoomControl.vue +7-2Lines changed: 7 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -3,16 +3,21 @@ import { Dropdown } from 'floating-vue' | 3 | 3 | import IconButton from '~/components/IconButton.vue' | 4 | 4 | import { useTraceZoom } from '~/composables/trace-view' | 5 | 5 |
| | 6 | +defineProps<{ | | 7 | + disabled?: boolean | | 8 | +}>() | | 9 | +
| 6 | 10 | const { level, percent, nextIn, nextOut, zoomIn, zoomOut, resetZoom, fit } = useTraceZoom() | 7 | 11 | </script> | 8 | 12 |
| 9 | 13 | <template> | 10 | | - <Dropdown placement="bottom-end"> | | 14 | + <Dropdown placement="bottom-end" :disabled="disabled"> | 11 | 15 | <button | 12 | 16 | type="button" | 13 | 17 | aria-label="Trace zoom" | 14 | 18 | data-testid="trace-zoom-trigger" | 15 | | - class="flex items-center gap-1 rounded px-1 text-xs tabular-nums op70 hover:bg-active hover:op100" | | 19 | + :disabled="disabled" | | 20 | + class="flex items-center gap-1 rounded px-1 text-xs tabular-nums op70 enabled:hover:bg-active enabled:hover:op100 disabled:op40 disabled:cursor-not-allowed" | 16 | 21 | > | 17 | 22 | <span class="i-carbon:zoom-in block" /> | 18 | 23 | <span>{{ percent }}</span> | |
Collapse file Copy file name to clipboardExpand all lines: packages/ui/client/composables/trace-view.ts +16Lines changed: 16 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -359,3 +359,19 @@ export function useTraceZoom() { | 359 | 359 |
| 360 | 360 | return { level, percent, nextIn, nextOut, zoomIn, zoomOut, resetZoom, fit, setFitContainer } | 361 | 361 | } | | 362 | + | | 363 | +const traceSnapshotViewMode = useLocalStorage<'dom' | 'aria'>( | | 364 | + 'vitest-ui_trace-snapshot-view-mode', | | 365 | + 'dom', | | 366 | +) | | 367 | + | | 368 | +export function useTraceSnapshotViewMode(getTrace: () => BrowserTraceData | undefined) { | | 369 | + const snapshotViewMode = traceSnapshotViewMode | | 370 | + const hasAriaSnapshot = computed( | | 371 | + () => !!getTrace()?.entries.some((entry) => entry.snapshot.ariaSnapshot != null), | | 372 | + ) | | 373 | + const showAriaSnapshot = computed( | | 374 | + () => snapshotViewMode.value === 'aria' && hasAriaSnapshot.value, | | 375 | + ) | | 376 | + return { snapshotViewMode, hasAriaSnapshot, showAriaSnapshot } | | 377 | +} |
Collapse file Copy file name to clipboardExpand all lines: packages/vitest/src/node/cli/cli-config.ts +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -428,6 +428,9 @@ export const cliOptionsConfig: VitestCLIOptions = { | 428 | 428 | inlineImages: { | 429 | 429 | description: 'Inline loaded image pixels in trace-view snapshots (default: `false`)', | 430 | 430 | }, | | 431 | + ariaSnapshot: { | | 432 | + description: 'Capture the accessibility tree in trace-view snapshots (default: `true`)', | | 433 | + }, | 431 | 434 | }, | 432 | 435 | }, | 433 | 436 | orchestratorScripts: null, | |
Collapse file Copy file name to clipboardExpand all lines: packages/vitest/src/node/config/resolveConfig.ts +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -944,11 +944,13 @@ export function resolveTestConfig( | 944 | 944 | enabled: traceView.enabled ?? false, | 945 | 945 | recordCanvas: traceView.recordCanvas ?? false, | 946 | 946 | inlineImages: traceView.inlineImages ?? false, | | 947 | + ariaSnapshot: traceView.ariaSnapshot ?? true, | 947 | 948 | } | 948 | 949 | : { | 949 | 950 | enabled: traceView ?? false, | 950 | 951 | recordCanvas: false, | 951 | 952 | inlineImages: false, | | 953 | + ariaSnapshot: true, | 952 | 954 | } | 953 | 955 | if (resolved.browser.enabled && resolved.browser.traceView.enabled) { | 954 | 956 | resolved.browser.detailsPanelPosition = 'bottom' | |
|