- Repo: https://github.com/hi-ogawa/vitest
- Commit: c3ba16b35847d27fb9f01f38cdcb047c77e121f8
- Branch: main
Rework Vitest Trace View so Vitest-owned DOM snapshots can be captured outside browser mode and displayed by the existing Vitest UI and HTML report. The motivating case is a Vitest Node test or custom E2E setup that uses the Playwright programmatic API, potentially through an injected client script.
The initial goal is not to ingest Playwright .trace.zip files or expose a general trace interchange format. Vitest owns both capture and replay.
The existing feature combines several layers that are currently tied together by browser-mode state.
- Action instrumentation wraps provider-backed browser commands in packages/browser/src/client/tester/tester-utils.ts:138. It emits start and end entries around actions when the current browser test has an active trace attempt.
- Snapshot capture lives in packages/browser/src/client/tester/trace.ts:88. It reads Vitest browser globals, resolves locators with Ivya, captures the DOM with patched
rrweb-snapshot, and immediately sends the result through browser RPC. - The browser command resolves source locations and records an
internal:browserTraceartifact in packages/browser/src/node/commands/trace.ts:63. - The UI groups artifacts by retry and repeat, then joins range start and end entries in packages/ui/client/composables/trace-view.ts:37.
- Replay rebuilds each selected rrweb snapshot in a sandboxed iframe, restores pseudo-state and scroll, and highlights the selected node in packages/ui/client/components/trace/TraceView.vue:61.
- Public artifact support already provides transport from a test worker to reporters and the main process through
recordArtifactin packages/vitest/src/runtime/runner/artifact.ts:41.
Vitest's Playwright tracing feature is separate. It delegates to Playwright tracing and stores .trace.zip attachments in packages/browser-playwright/src/commands/trace.ts:10. It should remain independent from the rrweb-based Trace View.
recordBrowserTraceEntryassumes a browser-mode runner task,getBrowserState(),getWorkerState(), an active attempt map, an Ivya selector engine, and Vitest browser RPC.- Snapshot capture and artifact submission are one operation. There is no primitive that only captures opaque trace data from an arbitrary page.
- Trace rendering is enabled according to browser project configuration rather than artifact presence in packages/ui/client/composables/trace-view.ts:204 and packages/ui/client/composables/trace-view.ts:246.
- The UI imports private capture types through a monorepo-relative path in packages/ui/client/composables/trace-view.ts:2.
BrowserTraceArtifact.dataremains deliberately opaque asunknownin packages/vitest/src/runtime/runner/types.ts:1481.- Trace entries are currently streamed as one artifact per entry, while the UI assembles the complete attempt. The code already notes that this representation may need revisiting in packages/browser/src/client/tester/trace.ts:12.
- Snapshot portability remains limited because there is no general resource store for fonts, CSS subresources, videos, and other URL-backed assets, as documented in docs/guide/browser/trace-view.md:177.
A public, versioned trace-data contract is not an initial requirement. That idea incorrectly treated external consumption as third-party trace production or interoperability.
For the intended integration, Vitest owns:
- The script injected into the page.
- The Node-side recorder/controller.
- The private artifact payload and transport.
- The rrweb rebuild implementation.
- The Vitest UI and static HTML viewer consuming the payload.
There will still be an internal wire shape between capture and replay, but both ends can evolve together. The recorder API should return or retain opaque data rather than make the snapshot schema public.
A format version may still be useful as an internal guard. A public compatibility contract only becomes necessary if Vitest later supports third-party producers, third-party viewers, standalone trace files opened by unrelated Vitest versions, or long-lived trace archives.
arbitrary browser page
-> run Vitest-owned capture script
-> capture an opaque snapshot payload
-> caller records trace entries against a Vitest task
-> artifact pipeline streams or persists it
-> Vitest UI rebuilds the snapshots
browser.traceView should control automatic collection in browser mode. It should not decide whether an existing trace artifact can be displayed. Display should be artifact-driven.
The browser-mode implementation and any future integrations should use the same low-level capture operation rather than maintain parallel rrweb capture paths.
The core primitive is a browser-side script. It captures the current document and returns an opaque, serializable snapshot payload. It does not know about Playwright, browser providers, Vitest tasks, RPC, fixtures, retries, or page lifecycle.
Illustrative browser-side shape:
const snapshot = captureTraceSnapshot({
element: document.querySelector('button'),
})The host records that payload separately:
await recordBrowserTrace(context.task, {
name: 'dashboard loaded',
kind: 'mark',
snapshot,
})The exact payload remains private and opaque. The browser-mode client can call the capture function directly. External integrations can inject or evaluate the same client script however they choose.
Range timing, pass or fail status, source locations, and lifecycle policy belong to the caller or a later ergonomic wrapper. The core snapshot function only needs capture options such as a browser-side DOM element, image inlining, and canvas recording.
A browser-side snapshot operation is the shared capability. Higher-level steps, assertions, and lifecycle capture can all be implemented later by invoking it at the appropriate points.
One explicit snapshot through custom injection tests the difficult boundaries first:
- Script injection by a custom integration.
- rrweb initialization in an arbitrary page.
- DOM and element-highlight snapshot fidelity.
- Cross-context transfer of potentially large snapshot payloads.
- Association with a Vitest task.
- Live artifact transport and HTML report persistence.
- Replay by the existing UI.
Once those work, step ranges, automatic lifecycle capture, assertions, and automation-library ergonomics are orchestration over the same snapshot operation.
The first experiment should require no Vitest core changes. Userland can temporarily reproduce the private browser-trace payload and record it through the existing public artifact API. Compatibility with future Vitest versions is explicitly not a goal for the spike.
- Run a normal Vitest test in a Node environment.
- Launch and control a page with the Playwright programmatic API.
- Install or evaluate userland snapshot code in that page. For a repository-local spike, reuse the workspace's patched
rrweb-snapshotdependency or copy the relevant current capture logic. - Access the current test task through the Vitest test context and call the public
recordArtifactAPI.
Capture the fields currently consumed by TraceView.vue:
serialized: the rrweb DOM snapshot.viewport: page width and height.scroll: page scroll position.pseudoClassIds: an empty object is sufficient initially.selectorId: optional and omitted for the first snapshot.
Record one complete entry with the current private envelope:
await recordArtifact(context.task, {
type: 'internal:browserTrace',
data: {
retry: 0,
repeats: 0,
recordCanvas: false,
entries: [{
name: 'userland snapshot',
kind: 'mark',
startTime: 0,
snapshot,
}],
},
})Using the reserved internal: artifact type and reproducing private data fields is acceptable only for this disposable experiment.
- Run the test with Vitest UI enabled.
- Select the test and open its Report tab.
- Confirm the existing Trace View artifact button appears.
- Open the trace and confirm the DOM snapshot rebuilds correctly.
- Record another snapshot after a Playwright action and confirm both steps appear in order.
- Run with the HTML reporter and confirm the static report contains and replays the same snapshots.
The manual artifact button already bypasses the browser.traceView.enabled check. Live artifact transport and HTML report serialization should also work without browser-mode configuration.
- Add an optional DOM element to capture and store its rrweb mirror ID as
selectorIdto validate highlighting. - Emit start and end entries with the same range ID to validate duration merging and in-progress display.
- Validate source navigation through generic artifact location metadata as described below.
- Capture final page state during normal fixture teardown.
- Try retries with explicit
retryvalues only after the single-attempt path works.
Trace recording should reuse the generic recordArtifact location mechanism rather than introduce a trace-specific stack parser or runtime RPC.
The userland recorder can use vi.defineHelper around the stock artifact call:
const recordTrace = vi.defineHelper(async (task, data) => {
await recordArtifact(task, {
type: 'internal:browserTrace',
data,
})
})The intended metadata flow is:
vi.defineHelpermarks the boundary between recorder internals and the user call site.recordArtifactresolves that call site and stores it asartifact.location.- Trace View uses
artifact.locationas the fallback for an entry without an explicitentry.location. - Existing trace source navigation and editor markers consume the resulting entry location.
There is a separate generic issue to resolve first: recordArtifact currently probes its raw stack through findTestFileStackTrace, while vi.defineHelper frame removal lives in the standard parseStacktrace pipeline. Stock recordArtifact and vi.defineHelper should compose regardless of Trace View. That generic behavior is being investigated separately.
The trace-specific change should remain in UI normalization. getTraceAttemptMap currently extracts artifact.data and discards the enclosing artifact location. When flattening trace entries, it should preserve an explicit browser-mode entry.location and otherwise inherit the artifact location:
location: entry.location ?? artifact.locationThis mapping relies on the current one-entry-per-artifact transport. That is also the natural model for streamed userland entries. If entries are batched later, each entry will need its own location or the batch must intentionally share one location.
Full editor integration also requires loosening the UI gutter condition. ViewEditor.vue currently configures trace gutters only when browser.traceView.enabled. Gutter availability should instead follow an active trace or available trace artifacts so a Node-environment trace can show markers.
The spike should cover location behavior in both live UI and the HTML report:
- Call a trace recorder wrapped with
vi.defineHelperfrom a known test line. - Select the corresponding trace step and verify the source editor opens at that call line.
- Verify the trace gutter marker appears and tracks the selected step.
- Keep browser-mode entries with explicit locations unchanged.
No dedicated trace runtime location API is planned. A future helper may hide the private trace artifact envelope, but call-site resolution should remain generic artifact behavior.
- A Node-environment Vitest test using programmatic Playwright can add trace entries without browser mode.
- The existing live UI and HTML reporter replay those entries without modification.
- Multiple snapshots preserve order and basic timing.
- The spike identifies whether rrweb payload transfer, report size, CSP, navigation, or snapshot fidelity creates a blocker.
- The spike depends on a private payload shape and may break on any Vitest update.
- It uses a reserved artifact discriminator without a sanctioned public helper.
- Injection, navigation persistence, task association, ranges, source locations, and teardown are userland responsibilities.
- Basic snapshots may omit pseudo-state, image inlining, canvas pixels, selector highlighting, and external resources.
- Timeouts, cancellation, worker termination, and browser crashes may lose the final snapshot.
If the spike works, core changes can remain focused on productization rather than feasibility. The first decisions are whether to sanction artifact recording, make trace discovery fully artifact-driven, and extract the owned capture script. If it does not work, make only the smallest core adjustment demonstrated by the failure.
This is a possible productization plan after the userland spike. It is not required to start the experiment.
- Extract a self-contained browser capture runtime from the current browser-state-dependent
takeSnapshotimplementation. - Expose one browser-side snapshot operation with capture options and an optional DOM element to highlight.
- Keep injection, evaluation, range orchestration, and task association outside the capture runtime.
- Keep the internal snapshot representation private and reuse the patched rrweb behavior required by the current viewer.
- Add a Vitest-side helper that accepts the opaque snapshot plus entry metadata and records the existing internal artifact.
- Let the UI recognize and open trace artifacts even when the test project did not enable
browser.traceView. - Separate collection configuration from rendering capability.
- Reuse the existing attempt grouping and range normalization.
- Extract only the replay operation from
TraceView.vueif needed so it consumes opaque normalized trace data without browser-mode assumptions. - Keep Vitest-specific selection, URL persistence, source navigation, and editor markers in the UI host layer.
- Route existing browser-mode actions, assertions, marks, and final lifecycle snapshots through the same recorder core.
- Retain browser-mode-specific instrumentation in
CommandsManagerand the browser runner. - Remove duplicate assumptions from the generic capture layer, especially current-task lookup and direct RPC access.
- Confirm retries, repeats, nested ranges, failures, and live in-progress entries preserve current behavior.
The core extraction ends here. Everything below is optional future ergonomics and does not shape the capture primitive.
Rstest provides relevant prior art in @rstest/playwright. It keeps tests in the Rstest Node runner while exposing Playwright-style test, fixtures, and retrying assertions:
import { expect, test } from '@rstest/playwright'
test('page title', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
await expect(page.locator('h1')).toHaveText('Example Domain')
})Its fixture model shares a browser within a worker and creates a context and page per test. Its expect retains normal runner assertions while recognizing Playwright Page and Locator values for retrying E2E assertions. This demonstrates that Playwright-like ergonomics can be layered onto a Vitest-style runner without adopting Playwright Test as the runner.
Rstest also keeps Playwright's official .trace.zip capture separate from its runner fixtures and assertions. That supports the same separation here: Vitest Trace View capture can remain its own rrweb feature even if a future Playwright-oriented package also offers Playwright-native tracing.
First introduce a Playwright-specific controller only when working on ergonomic integration. It can own script injection, page.evaluate, Playwright locator resolution, step ranges, and automatic artifact submission.
Then provide a Vitest fixture that mirrors the familiar Playwright page fixture while initializing and finalizing the controller automatically.
Illustrative shape:
const test = base.extend({
page: async ({ page }, use, context) => {
const tracedPage = await createTracedPage(page, context.task)
await use(tracedPage)
await tracedPage.trace.finish()
},
})The fixture can provide automatic final snapshots and attachment during teardown without requiring a custom runner. Its limits around hard timeouts, crashes, interrupted teardown, and retries should be evaluated before promising complete lifecycle coverage.
- Add page and locator assertions suited to Node-side Playwright E2E tests.
- Record assertion start and end snapshots, status, timing, highlighted locator, and source location.
- Model the ergonomics after browser-mode
expect.element, but emit through the generic recorder. - Avoid coupling the recorder core to a particular assertion library.
Consider a dedicated Vitest runner or runner extension if fixtures cannot reliably cover test start, retries, final snapshots, failures, timeouts, cancellation, browser crashes, and unfinished trace operations.
The runner should orchestrate the same recorder primitive. It should not introduce a second capture or artifact format.
Evaluate selective wrapping of common Playwright Page and Locator operations after the explicit and assertion APIs establish expected behavior. Full transparent wrapping should not be an initial requirement because it would closely track Playwright's surface and internals.
- Extract an injectable browser capture script with no Playwright or runner dependency.
- Use a small test-only custom injection to capture one explicit snapshot from a normal Vitest Node test.
- Record the opaque result against
context.taskusing a dedicated helper overrecordArtifact. - Make the Vitest UI display the artifact without browser configuration.
- Verify the same result in the static HTML reporter.
- Verify browser mode can call the same capture operation without changing existing trace behavior.
This vertical slice productizes the path already validated by the userland spike while leaving step orchestration, Playwright adapters, fixtures, assertions, and runner ergonomics for later iterations.
The core is an injectable browser-side capture script. It has no Playwright Page assumption and no generic automation-host abstraction. Each caller decides how to load and invoke it.
Record each completed entry through the existing one-artifact-per-entry path. This preserves live UI updates and avoids introducing a second assembly model. The capture script only returns snapshot data; the caller decides when an entry is complete.
The core has no recorder-to-page lifecycle. Navigation persistence, page replacement, popups, additional tabs, frames, and browser-context-wide traces belong to custom injection code or the later Playwright controller.
The capture script produces snapshot data only. The caller supplies names, kinds, range IDs, timing, status, and optional stacks or locations when recording an entry. Future wrappers should capture stacks at the user-facing API call and resolve them through Vitest's existing source-map machinery.
The capture script accepts an optional browser-side DOM element and records its rrweb mirror ID. Translating Ivya selectors, Playwright locators, or another host's locator objects into that element belongs outside the core. The replay UI continues to highlight by mirror ID and remains provider-independent.
The capture script has no teardown responsibility. Normal completion, hard timeouts, cancellation, worker termination, browser crashes, and emergency flushing belong to custom integration code, fixtures, or the later runner integration.
Keep snapshots inline in trace artifacts until report size or transport measurements demonstrate a problem. Do not introduce a trace bundle, binary attachment, or resource store in the first implementation.
- Parsing or translating Playwright
.trace.zipfiles. - Replacing Playwright Trace Viewer.
- Exposing rrweb's serialized representation as a stable public API.
- Supporting third-party trace renderers.
- Automatically intercepting every Playwright operation.
- Solving general network and external-resource replay in the first iteration.