Skip to content
Draft
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
12 changes: 11 additions & 1 deletion docs/config/browser/traceview.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ outline: deep

# browser.traceView <Badge type="warning" text="Experimental" /> <Version>5.0.0</Version>

- **Type:** `boolean | { enabled?: boolean; recordCanvas?: boolean; inlineImages?: boolean }`
- **Type:** `boolean | { enabled?: boolean; snapshot?: 'always' | 'on-failure'; recordCanvas?: boolean; inlineImages?: boolean }`
- **CLI:** `--browser.traceView`
- **Default:** `false`

Expand All @@ -29,6 +29,7 @@ export default defineConfig({
browser: {
traceView: {
enabled: true,
snapshot: 'always',
inlineImages: true,
recordCanvas: true,
},
Expand All @@ -40,6 +41,7 @@ export default defineConfig({
| Option | Default | Description |
| --- | --- | --- |
| `enabled` | `false` | Enables Vitest trace-view artifact collection. |
| `snapshot` | `'always'` | Controls when DOM snapshots are captured for trace-view entries. |
| `inlineImages` | `false` | Inlines loaded `<img>` pixels into snapshots for more portable replay, useful in the HTML reporter. |
| `recordCanvas` | `false` | Captures canvas pixels in snapshots. |

Expand All @@ -51,6 +53,14 @@ export default defineConfig({

Enables Vitest trace-view artifact collection.

## browser.traceView.snapshot {#traceview-snapshot}

- **Type:** `'always' | 'on-failure'`
- **Default:** `'always'`
- **CLI:** `--browser.traceView.snapshot <policy>`

Controls when DOM snapshots are captured for trace-view entries.

## browser.traceView.inlineImages {#traceview-inlineimages}

- **Type:** `boolean`
Expand Down
7 changes: 7 additions & 0 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,13 @@ Enable trace view mode. Supported: "on", "off", "on-first-retry", "on-all-retrie

Enable Vitest trace-view collection for browser tests (default: `false`)

### browser.traceView.snapshot

- **CLI:** `--browser.traceView.snapshot <policy>`
- **Config:** [browser.traceView.snapshot](/config/browser/traceview#traceview-snapshot)

Control when DOM snapshots are captured for trace-view entries. Supported: "always", "on-failure" (default: `"always"`)

### browser.traceView.recordCanvas

- **CLI:** `--browser.traceView.recordCanvas`
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function createBrowserRunner(
return
}
if (shouldTraceView) {
getBrowserState().browserTraceDomSnapshot = await import('rrweb-snapshot')
getBrowserState().browserTraceDomSnapshot ??= () => import('rrweb-snapshot')
getBrowserState().browserTraceAttempts.set(test.id, { retry, repeats, startTime: now() })
}
else {
Expand Down
14 changes: 9 additions & 5 deletions packages/browser/src/client/tester/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface BrowserTraceEntry {
// resolved server-side from stack in __vitest_recordBrowserTrace command
location?: { file: string; line: number; column: number }
element?: SerializedLocator
snapshot: TraceSnapshot
snapshot?: TraceSnapshot
}

interface TraceSnapshot {
Expand Down Expand Up @@ -89,14 +89,18 @@ export async function recordBrowserTraceEntry(
): Promise<void> {
const attemptInfo = getBrowserState().browserTraceAttempts.get(task.id)!
const relativeStartTime = now() - attemptInfo.startTime
const snapshot = takeSnapshot(options.element)
const traceView = getBrowserState().config.browser.traceView
const shouldTakeSnapshot
= traceView.snapshot === 'always'
|| (traceView.snapshot === 'on-failure' && options.status === 'fail')
const snapshot = shouldTakeSnapshot ? await takeSnapshot(options.element) : undefined
const entry: BrowserTraceEntry = {
...options,
startTime: relativeStartTime,
snapshot,
}
const { retry, repeats } = attemptInfo
const { recordCanvas } = getBrowserState().config.browser.traceView
const { recordCanvas } = traceView

// An async lane could defer artifact recording and flush it at test-attempt end,
// but the synchronous snapshot work is already a comparable cost, and this path
Expand All @@ -123,8 +127,8 @@ export async function recordBrowserTraceEntry(
// selector engine inside the snapshot iframe at view time via injected script.
// Our approach resolves at collection time (same moment as snapshot) — simpler but
// requires Mirror plumbing. nodeId-based lookup also works across shadow DOM, unlike querySelector.
function takeSnapshot(serializedLocator?: SerializedLocator): TraceSnapshot {
const { snapshot, createMirror } = getBrowserState().browserTraceDomSnapshot!
async function takeSnapshot(serializedLocator?: SerializedLocator): Promise<TraceSnapshot> {
const { snapshot, createMirror } = await getBrowserState().browserTraceDomSnapshot!()
const traceView = getBrowserState().config.browser.traceView
const engine = getBrowserState().selectorEngine!
const mirror = createMirror()
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export interface BrowserRunnerState {
commands: CommandsManager
activeTraceTaskIds: Set<string>
browserTraceAttempts: Map<string, BrowserTraceAttempt>
// lazily loaded only when traceView is enabled
browserTraceDomSnapshot?: typeof import('rrweb-snapshot')
// lazily loaded only when traceView captures a snapshot
browserTraceDomSnapshot?: () => Promise<typeof import('rrweb-snapshot')>
selectorEngine: Ivya
traces: Traces
cleanups: Array<() => unknown>
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/client/components/trace/TraceView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function onSelectStep(index: number) {
}

watch([selectedStep, iframeEl], ([step, iframe]) => {
if (!step || !iframe) {
if (!step?.snapshot || !iframe) {
return
}
const { serialized, selectorId, viewport, scroll, pseudoClassIds } = step.snapshot
Expand Down Expand Up @@ -189,12 +189,15 @@ function isTraceStepInProgress(step: BrowserTraceEntry) {
<Pane :size="70" min-size="20">
<div class="h-full min-h-0" flex="~ col" overflow-auto>
<iframe
v-if="selectedStep"
v-if="selectedStep?.snapshot"
ref="iframeEl"
:key="iframeSandbox"
:sandbox="iframeSandbox"
style="background: white; border: none; color-scheme: normal; flex: none"
/>
<div v-else-if="selectedStep" class="text-sm opacity-50 p-4">
No DOM snapshot captured for this trace step
</div>
<div v-else class="text-sm opacity-50 p-4">
No trace step found
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/client/composables/trace-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function mergeTraceRangeEntries(entries: BrowserTraceEntry[]): BrowserTraceEntry
...entry,
startTime: start.startTime,
duration: entry.startTime - start.startTime,
snapshot: entry.snapshot ?? start.snapshot,
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ export const cliOptionsConfig: VitestCLIOptions = {
enabled: {
description: 'Enable Vitest trace-view collection for browser tests (default: `false`)',
},
snapshot: {
description: 'Control when DOM snapshots are captured for trace-view entries. Supported: "always", "on-failure" (default: `"always"`)',
argument: '<policy>',
},
recordCanvas: {
description: 'Capture canvas pixels in trace-view snapshots (default: `false`)',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,11 +902,13 @@ export function resolveConfig(
resolved.browser.traceView = typeof traceView === 'object'
? {
enabled: traceView.enabled ?? false,
snapshot: traceView.snapshot ?? 'always',
recordCanvas: traceView.recordCanvas ?? false,
inlineImages: traceView.inlineImages ?? false,
}
: {
enabled: traceView ?? false,
snapshot: 'always',
recordCanvas: false,
inlineImages: false,
}
Expand Down
8 changes: 8 additions & 0 deletions packages/vitest/src/node/types/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ export interface BrowserTraceViewOptions {
*/
enabled?: boolean

/**
* Control when DOM snapshots are captured for trace-view entries.
*
* @default 'always'
* @experimental
*/
snapshot?: 'always' | 'on-failure'

/**
* Capture canvas pixels in trace view snapshots.
*
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface SerializedConfig {
trace: BrowserTraceViewMode
traceView: {
enabled: boolean
snapshot: 'always' | 'on-failure'
recordCanvas: boolean
inlineImages: boolean
}
Expand Down
4 changes: 2 additions & 2 deletions test/browser/specs/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ test('trace view artifacts', async () => {
startTime: undefined,
duration: undefined,
snapshot: {
selectorResolution: raw.snapshot.selectorResolution,
selectorError: raw.snapshot.selectorError,
selectorResolution: raw.snapshot?.selectorResolution,
selectorError: raw.snapshot?.selectorError,
},
}
// remove noisy undefined properties
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions test/ui/fixtures/trace-bench/mark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from "vitest"

test("marks", () => {})
22 changes: 22 additions & 0 deletions test/ui/fixtures/trace-bench/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
ui: true,
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
headless: true,
ui: false,
traceView: {
enabled: true,
// snapshot: 'on-failure',
},
screenshotFailures: false,
},
},
})
15 changes: 15 additions & 0 deletions test/ui/fixtures/trace-on-failure/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'

test('basic', async () => {
document.body.innerHTML = '<button>hello</button>'
await expect
.element(page.getByRole('button'))
.toHaveTextContent('hello')
await expect
.element(page.getByRole('button'))
.toHaveAccessibleName('hello')
await expect
.element(page.getByRole('button'), { timeout: 100 })
.toHaveTextContent('world')
})
22 changes: 22 additions & 0 deletions test/ui/fixtures/trace-on-failure/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
ui: true,
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
headless: true,
ui: false,
traceView: {
enabled: true,
snapshot: 'on-failure',
},
screenshotFailures: false,
},
},
})
51 changes: 51 additions & 0 deletions test/ui/test/trace-on-failure.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Vitest } from 'vitest/node'
import { expect, test } from '@playwright/test'
import { assertTestCounts, openExplorerItem, startVitestUi } from './helper'

test.describe('ui on-failure snapshots', () => {
let vitest: Vitest | undefined
let baseURL: string

test.beforeAll(async () => {
const root = './fixtures/trace-on-failure'
const server = await startVitestUi({
root,
watch: true,
ui: true,
open: false,
})
vitest = server.vitest
baseURL = `${server.url}/__vitest__/`
})

test.afterAll(async () => {
await vitest?.close()
})

test.beforeEach(async ({ page }) => {
await page.goto(baseURL)
await assertTestCounts(page, { pass: 0, fail: 1 })
})

test('basic', async ({ page }) => {
await openExplorerItem(page, 'basic')

const traceView = page.getByTestId('trace-view')
await expect(traceView).toBeVisible()

const traceSteps = traceView.getByTestId('trace-step-name')
await expect(traceSteps).toHaveText([
'toHaveTextContent',
'toHaveAccessibleName',
'toHaveTextContent [ERROR]',
'test finished',
])

await traceSteps.nth(2).click()
const traceFrame = traceView.frameLocator('iframe')
await expect(traceFrame.getByRole('button', { name: 'hello' })).toBeVisible()

await traceSteps.nth(1).click()
await expect(traceView.getByText('No DOM snapshot captured for this trace step')).toBeVisible()
})
})
Loading