Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6395c96
feat: improve playwright trace
hi-ogawa Feb 13, 2026
276210c
chore: todo
hi-ogawa Feb 13, 2026
409baf7
feat: markTrace on onTaskFinished errors
hi-ogawa Feb 13, 2026
0798165
docs: revert
hi-ogawa Feb 13, 2026
b7c75ef
fix: pw only
hi-ogawa Feb 13, 2026
2061704
Merge branch 'main' into 02-13-feat_improve_playwright_trace
hi-ogawa Feb 17, 2026
4c740e7
fix: try/catch trace hack
hi-ogawa Feb 17, 2026
f781463
feat: support markTrace with locator
hi-ogawa Feb 17, 2026
6c17817
chore: example
hi-ogawa Feb 17, 2026
1ae2e5c
feat: page.markTrace and locator.markTrace
hi-ogawa Feb 17, 2026
7a402a5
example
hi-ogawa Feb 17, 2026
e108db8
chore: cleanup
hi-ogawa Feb 17, 2026
23971d5
fix: make markTrace no-op when trace is not active
hi-ogawa Feb 17, 2026
a746f34
chore: cleanup
hi-ogawa Feb 17, 2026
88c57a8
fix: trace on onAfterRetryTask
hi-ogawa Feb 17, 2026
6b67fe1
chore: example
hi-ogawa Feb 17, 2026
8ef1094
feat: call trace on expect.element
hi-ogawa Feb 17, 2026
b07c25d
refactor: types
hi-ogawa Feb 17, 2026
c970d86
refactor: cleanup
hi-ogawa Feb 17, 2026
5c67b06
Merge branch 'main' into 02-13-feat_improve_playwright_trace
hi-ogawa Feb 18, 2026
a7acfc4
fix: include sources
hi-ogawa Feb 18, 2026
4651fd7
feat: simplify browser trace mark API
hi-ogawa Feb 18, 2026
b606490
docs: tweak
hi-ogawa Feb 18, 2026
9e58b72
docs: add browser trace mark documentation
hi-ogawa Feb 18, 2026
8169fbd
docs: add mark API links in JSDoc
hi-ogawa Feb 18, 2026
f2cd343
docs: add locator.describe tracing notes
hi-ogawa Feb 19, 2026
6884131
Revert "docs: add locator.describe tracing notes"
hi-ogawa Feb 19, 2026
03d4c2e
test: wip
hi-ogawa Feb 19, 2026
9706e90
test: wip
hi-ogawa Feb 19, 2026
524d078
test: wip
hi-ogawa Feb 19, 2026
80d42c5
chore: lint
hi-ogawa Feb 19, 2026
d721d32
test: refactor
hi-ogawa Feb 19, 2026
a409b3e
test: branch webkit
hi-ogawa Feb 19, 2026
32067b5
test: refactor
hi-ogawa Feb 19, 2026
538e537
test: test vi.defineHelper
hi-ogawa Feb 19, 2026
c7087bd
feat: support custom stack
hi-ogawa Feb 19, 2026
c529452
feat: describe browser-playwright locators in traces
hi-ogawa Feb 19, 2026
3763d0b
feat: support mark(name, () => {})
hi-ogawa Feb 19, 2026
9faec43
chore: remove markGroup
hi-ogawa Feb 19, 2026
85ca927
test: test mark group
hi-ogawa Feb 19, 2026
e9da4cf
docs: no limitations
hi-ogawa Feb 19, 2026
0435861
chore: example
hi-ogawa Feb 20, 2026
38fea7c
docs: warning playwright#39307
hi-ogawa Feb 20, 2026
a99ea35
test: add test/browser/README.md
hi-ogawa Feb 20, 2026
34f83b3
chore: comment
hi-ogawa Feb 20, 2026
959aea4
fix: re-export asLocator from @vitest/browser
hi-ogawa Feb 20, 2026
9bed8b3
Merge branch 'main' into 02-13-feat_improve_playwright_trace
hi-ogawa Feb 21, 2026
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
42 changes: 42 additions & 0 deletions docs/api/browser/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export const page: {
base64: string
}>
screenshot(options?: ScreenshotOptions): Promise<string>
/**
* Add a trace marker when browser tracing is enabled.
*/
mark(name: string, options?: { stack?: string }): Promise<void>
/**
* Group multiple operations under a trace marker when browser tracing is enabled.
*/
mark<T>(name: string, body: () => T | Promise<T>, options?: { stack?: string }): Promise<T>
/**
* Extend default `page` object with custom methods.
*/
Expand Down Expand Up @@ -116,6 +124,40 @@ Note that `screenshot` will always return a base64 string if `save` is set to `f
The `path` is also ignored in that case.
:::

### mark

```ts
function mark(name: string, options?: { stack?: string }): Promise<void>
function mark<T>(
name: string,
body: () => T | Promise<T>,
options?: { stack?: string },
): Promise<T>
```

Adds a named marker to the trace timeline for the current test.

Pass `options.stack` to override the callsite location in trace metadata. This is useful for wrapper libraries that need to preserve the end-user source location.

If you pass a callback, Vitest creates a trace group with this name, runs the callback, and closes the group automatically.

```ts
import { page } from 'vitest/browser'

await page.mark('before submit')
await page.getByRole('button', { name: 'Submit' }).click()
await page.mark('after submit')

await page.mark('submit flow', async () => {
await page.getByRole('textbox', { name: 'Email' }).fill('john@example.com')
await page.getByRole('button', { name: 'Submit' }).click()
})
```

::: tip
This method is useful only when [`browser.trace`](/config/browser/trace) is enabled.
:::

### frameLocator

```ts
Expand Down
24 changes: 24 additions & 0 deletions docs/api/browser/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,30 @@ Note that `screenshot` will always return a base64 string if `save` is set to `f
The `path` is also ignored in that case.
:::

### mark

```ts
function mark(name: string, options?: { stack?: string }): Promise<void>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stack? option is added so that vitest-browser-xxx render helper can manipulate the trace. see vitest-community/vitest-browser-react#47

```

Adds a named marker to the trace timeline and uses the current locator as marker context.

Pass `options.stack` to override the callsite location in trace metadata. This is useful for wrapper libraries that need to preserve the end-user source location.

```ts
import { page } from 'vitest/browser'

const submitButton = page.getByRole('button', { name: 'Submit' })

await submitButton.mark('before submit')
await submitButton.click()
await submitButton.mark('after submit')
```

::: tip
This method is useful only when [`browser.trace`](/config/browser/trace) is enabled.
:::

### query

```ts
Expand Down
61 changes: 59 additions & 2 deletions docs/guide/browser/trace-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ export default defineConfig({

The traces are available in reporters as [annotations](/guide/test-annotations). For example, in the HTML reporter, you can find the link to the trace file in the test details.

## Trace markers

You can add explicit named markers to make the trace timeline easier to read:

```ts
import { page } from 'vitest/browser'

document.body.innerHTML = `
<button type="button">Sign in</button>
`

await page.getByRole('button', { name: 'Sign in' }).mark('sign in button rendered')
```

Both `page.mark(name)` and `locator.mark(name)` are available.

You can also group multiple operations under one marker with `page.mark(name, callback)`:

```ts
await page.mark('sign in flow', async () => {
await page.getByRole('textbox', { name: 'Email' }).fill('john@example.com')
await page.getByRole('textbox', { name: 'Password' }).fill('secret')
await page.getByRole('button', { name: 'Sign in' }).click()
})
```

You can also wrap reusable helpers with [`vi.defineHelper()`](/api/vi#vi-defineHelper) so trace entries point to where the helper is called, not its internals:

```ts
import { vi } from 'vitest'
import { page } from 'vitest/browser'

const myRender = vi.defineHelper(async (content: string) => {
document.body.innerHTML = content
await page.elementLocator(document.body).mark('render helper')
})

test('renders content', async () => {
await myRender('<button>Hello</button>') // trace points to this line
})
```

## Preview

To open the trace file, you can use the Playwright Trace Viewer. Run the following command in your terminal:
Expand All @@ -69,6 +111,21 @@ This will start the Trace Viewer and load the specified trace file.

Alternatively, you can open the Trace Viewer in your browser at https://trace.playwright.dev and upload the trace file there.

## Limitations
## Source Location

When you open a trace, you'll notice that Vitest groups browser interactions and links them back to the exact line in your test that triggered them. This happens automatically for:

- `expect.element(...)` assertions
- Interactive actions like `click`, `fill`, `type`, `hover`, `selectOptions`, `upload`, `dragAndDrop`, `tab`, `keyboard`, `wheel`, and screenshots

Under the hood, Playwright still records its own low-level action events as usual. Vitest wraps them with source-location groups so you can jump straight from the trace timeline to the relevant line in your test.

At the moment, Vitest cannot populate the "Sources" tab in the Trace Viewer. This means that while you can see the actions and screenshots captured during the test, you won't be able to view the source code of your tests directly within the Trace Viewer. You will need to refer back to your code editor to see the test implementation.
Keep in mind that plain assertions like `expect(value).toBe(...)` run in Node, not the browser, so they won't show up in the trace.

For anything not covered automatically, you can use `page.mark()` or `locator.mark()` to add your own trace groups — see [Trace markers](#trace-markers) above.

::: warning

Currently a source view of a trace can be only displayed properly when viewing it on the machine generated a trace with `playwright show-trace` CLI. This is expected to be fixed soon (see https://github.com/microsoft/playwright/pull/39307).

:::
2 changes: 2 additions & 0 deletions examples/lit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__traces__
__screenshots__
10 changes: 8 additions & 2 deletions examples/lit/test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import { page } from 'vitest/browser'
import '../src/my-button.js'

describe('Button with increment', async () => {
beforeEach(() => {
document.body.innerHTML = '<my-button name="World"></my-button>'
beforeEach(async () => {
await page.mark('render', async () => {
document.body.innerHTML = '<my-button name="World"></my-button>'
await page.getByRole('button').mark('render button')
})
})

it('should increment the count on each click', async () => {
await page.getByRole('button').click()

await expect.element(page.getByRole('button')).toHaveTextContent('2')
if (import.meta.env.VITE_FAIL_TEST) {
await expect.element(page.getByRole('button'), { timeout: 3000 }).toHaveTextContent('3')
}
})

it('should show name props', async () => {
Expand Down
1 change: 1 addition & 0 deletions examples/lit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"experimentalDecorators": true,
"module": "node16",
"moduleResolution": "Node16",
"types": ["vite/client"],
"verbatimModuleSyntax": true
}
}
4 changes: 2 additions & 2 deletions packages/browser-playwright/src/commands/clear.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { UserEvent } from 'vitest/browser'
import type { UserEventCommand } from './utils'
import { getDescribedLocator } from './utils'

export const clear: UserEventCommand<UserEvent['clear']> = async (
context,
selector,
) => {
const { iframe } = context
const element = iframe.locator(selector)
const element = getDescribedLocator(context, selector)
await element.clear()
}
10 changes: 4 additions & 6 deletions packages/browser-playwright/src/commands/click.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import type { UserEvent } from 'vitest/browser'
import type { UserEventCommand } from './utils'
import { getDescribedLocator } from './utils'

export const click: UserEventCommand<UserEvent['click']> = async (
context,
selector,
options = {},
) => {
const tester = context.iframe
await tester.locator(selector).click(options)
await getDescribedLocator(context, selector).click(options)
}

export const dblClick: UserEventCommand<UserEvent['dblClick']> = async (
context,
selector,
options = {},
) => {
const tester = context.iframe
await tester.locator(selector).dblclick(options)
await getDescribedLocator(context, selector).dblclick(options)
}

export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async (
context,
selector,
options = {},
) => {
const tester = context.iframe
await tester.locator(selector).click({
await getDescribedLocator(context, selector).click({
...options,
clickCount: 3,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/browser-playwright/src/commands/fill.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { UserEvent } from 'vitest/browser'
import type { UserEventCommand } from './utils'
import { getDescribedLocator } from './utils'

export const fill: UserEventCommand<UserEvent['fill']> = async (
context,
selector,
text,
options = {},
) => {
const { iframe } = context
const element = iframe.locator(selector)
const element = getDescribedLocator(context, selector)
await element.fill(text, options)
}
3 changes: 2 additions & 1 deletion packages/browser-playwright/src/commands/hover.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { UserEvent } from 'vitest/browser'
import type { UserEventCommand } from './utils'
import { getDescribedLocator } from './utils'

export const hover: UserEventCommand<UserEvent['hover']> = async (
context,
selector,
options = {},
) => {
await context.iframe.locator(selector).hover(options)
await getDescribedLocator(context, selector).hover(options)
}
6 changes: 6 additions & 0 deletions packages/browser-playwright/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { tab } from './tab'
import {
annotateTraces,
deleteTracing,
groupTraceEnd,
groupTraceStart,
markTrace,
startChunkTrace,
startTracing,
stopChunkTrace,
Expand Down Expand Up @@ -39,4 +42,7 @@ export default {
__vitest_startTracing: startTracing as typeof startTracing,
__vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace,
__vitest_annotateTraces: annotateTraces as typeof annotateTraces,
__vitest_markTrace: markTrace as typeof markTrace,
__vitest_groupTraceStart: groupTraceStart as typeof groupTraceStart,
__vitest_groupTraceEnd: groupTraceEnd as typeof groupTraceEnd,
}
7 changes: 4 additions & 3 deletions packages/browser-playwright/src/commands/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BrowserCommandContext } from 'vitest/node'
import { mkdir } from 'node:fs/promises'
import { resolveScreenshotPath } from '@vitest/browser'
import { dirname, normalize } from 'pathe'
import { getDescribedLocator } from './utils'

interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> {
element?: string
Expand Down Expand Up @@ -41,11 +42,11 @@ export async function takeScreenshot(
await mkdir(dirname(savePath), { recursive: true })
}

const mask = options.mask?.map(selector => context.iframe.locator(selector))
const mask = options.mask?.map(selector => getDescribedLocator(context, selector))

if (options.element) {
const { element: selector, ...config } = options
const element = context.iframe.locator(selector)
const element = getDescribedLocator(context, selector)
const buffer = await element.screenshot({
...config,
mask,
Expand All @@ -54,7 +55,7 @@ export async function takeScreenshot(
return { buffer, path }
}

const buffer = await context.iframe.locator('body').screenshot({
const buffer = await getDescribedLocator(context, 'body').screenshot({
...options,
mask,
path: savePath,
Expand Down
6 changes: 3 additions & 3 deletions packages/browser-playwright/src/commands/select.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ElementHandle } from 'playwright'
import type { UserEvent } from 'vitest/browser'
import type { UserEventCommand } from './utils'
import { getDescribedLocator } from './utils'

export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async (
context,
Expand All @@ -9,14 +10,13 @@ export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async
options = {},
) => {
const value = userValues as any as (string | { element: string })[]
const { iframe } = context
const selectElement = iframe.locator(selector)
const selectElement = getDescribedLocator(context, selector)

const values = await Promise.all(value.map(async (v) => {
if (typeof v === 'string') {
return v
}
const elementHandler = await iframe.locator(v.element).elementHandle()
const elementHandler = await getDescribedLocator(context, v.element).elementHandle()
if (!elementHandler) {
throw new Error(`Element not found: ${v.element}`)
}
Expand Down
Loading
Loading