Skip to content
Merged
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
10 changes: 10 additions & 0 deletions packages/playwright-core/src/tools/trace/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
[*]
../../utils/isomorphic/**
../../server/utils/zipFile.ts
./*.ts

[traceSnapshot.ts]
../../..
../../utils
../backend/browserBackend.ts
../backend/tools.ts
../cli-daemon/command.ts
../cli-daemon/commands.ts
../cli-client/minimist.ts
102 changes: 57 additions & 45 deletions packages/playwright-core/src/tools/trace/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,41 @@ Inspect `.zip` trace files produced by Playwright tests without opening a browse

## Workflow

1. Start with `trace info` to understand what's in the trace.
1. Start with `trace open <trace.zip>` to extract the trace and see its metadata.
2. Use `trace actions` to see all actions with their action IDs.
3. Use `trace action <action-id>` to drill into a specific action — see parameters, logs, source location, and available snapshots.
4. Use `trace requests`, `trace console`, or `trace errors` for cross-cutting views.
5. Use `trace snapshot` or `trace screenshot` to extract visual state.
5. Use `trace snapshot <action-id>` to get the DOM snapshot, or run a browser command against it.

All commands after `open` operate on the currently opened trace — no need to pass the trace file again. Opening a new trace replaces the previous one.

## Commands

### Overview
### Open a trace

```bash
# Trace metadata: browser, viewport, duration, action/error counts
npx playwright trace info <trace.zip>
# Extract trace and show metadata: browser, viewport, duration, action/error counts
npx playwright trace open <trace.zip>
```

### Actions

```bash
# List all actions as a tree with action IDs and timing
npx playwright trace actions <trace.zip>
npx playwright trace actions

# Filter by action title (regex, case-insensitive)
npx playwright trace actions --grep "click" <trace.zip>
npx playwright trace actions --grep "click"

# Only failed actions
npx playwright trace actions --errors-only <trace.zip>
npx playwright trace actions --errors-only
```

### Action details

```bash
# Show full details for one action: params, result, logs, source, snapshots
npx playwright trace action <trace.zip> <action-id>
npx playwright trace action <action-id>
```

The `action` command displays available snapshot phases (before, input, after) and the exact command to extract them.
Expand All @@ -51,101 +53,111 @@ The `action` command displays available snapshot phases (before, input, after) a

```bash
# All network requests: method, status, URL, duration, size
npx playwright trace requests <trace.zip>
npx playwright trace requests

# Filter by URL pattern
npx playwright trace requests --grep "api" <trace.zip>
npx playwright trace requests --grep "api"

# Filter by HTTP method
npx playwright trace requests --method POST <trace.zip>
npx playwright trace requests --method POST

# Only failed requests (status >= 400)
npx playwright trace requests --failed <trace.zip>
npx playwright trace requests --failed
```

### Request details

```bash
# Show full details for one request: headers, body, security
npx playwright trace request <trace.zip> <request-id>
npx playwright trace request <request-id>
```

### Console

```bash
# All console messages and stdout/stderr
npx playwright trace console <trace.zip>
npx playwright trace console

# Only errors
npx playwright trace console --errors-only <trace.zip>
npx playwright trace console --errors-only

# Only browser console (no stdout/stderr)
npx playwright trace console --browser <trace.zip>
npx playwright trace console --browser

# Only stdout/stderr (no browser console)
npx playwright trace console --stdio <trace.zip>
npx playwright trace console --stdio
```

### Errors

```bash
# All errors with stack traces and associated actions
npx playwright trace errors <trace.zip>
npx playwright trace errors
```

### Snapshots

The `snapshot` command loads the DOM snapshot for an action into a headless browser and runs a single browser command against it. Without a browser command, it returns the accessibility snapshot.

```bash
# Save DOM snapshot as HTML (tries input, then before, then after)
npx playwright trace snapshot <trace.zip> <action-id> -o snapshot.html
# Get the accessibility snapshot (default)
npx playwright trace snapshot <action-id>

# Save a specific phase
npx playwright trace snapshot --name before <trace.zip> <action-id> -o before.html
npx playwright trace snapshot --name after <trace.zip> <action-id> -o after.html
# Use a specific phase
npx playwright trace snapshot <action-id> --name before

# Serve snapshot on localhost with resources
npx playwright trace snapshot --serve <trace.zip> <action-id>
```
# Run eval to query the DOM
npx playwright trace snapshot <action-id> -- eval "document.title"
npx playwright trace snapshot <action-id> -- eval "document.querySelector('#error').textContent"

### Screenshots
# Eval on a specific element ref (from the snapshot)
npx playwright trace snapshot <action-id> -- eval "el => el.getAttribute('data-testid')" e5

```bash
# Save the closest screencast frame for an action
npx playwright trace screenshot <trace.zip> <action-id> -o screenshot.png
# Take a screenshot of the snapshot
npx playwright trace snapshot <action-id> -- screenshot

# Redirect output to a file
npx playwright trace snapshot <action-id> -- eval "document.body.outerHTML" > page.html
npx playwright trace snapshot <action-id> -- screenshot > screenshot.png
```

Only three browser commands are useful on a frozen snapshot: `snapshot`, `eval`, and `screenshot`.

### Attachments

```bash
# List all trace attachments
npx playwright trace attachments <trace.zip>
npx playwright trace attachments

# Extract an attachment by its number
npx playwright trace attachment <trace.zip> 1
npx playwright trace attachment <trace.zip> 1 -o out.png
npx playwright trace attachment 1
npx playwright trace attachment 1 -o out.png
```

## Typical investigation

```bash
# 1. What happened in this trace?
npx playwright trace info test-results/my-test/trace.zip
# 1. Open the trace and see what's inside
npx playwright trace open test-results/my-test/trace.zip

# 2. What actions ran?
npx playwright trace actions test-results/my-test/trace.zip
npx playwright trace actions

# 3. Which action failed?
npx playwright trace actions --errors-only test-results/my-test/trace.zip
npx playwright trace actions --errors-only

# 4. What went wrong?
npx playwright trace action test-results/my-test/trace.zip 12
npx playwright trace action 12

# 5. What did the page look like at that moment?
npx playwright trace snapshot 12

# 5. What did the page look like?
npx playwright trace snapshot test-results/my-test/trace.zip 12 -o page.html
# 6. Query the DOM for more detail
npx playwright trace snapshot 12 -- eval "document.querySelector('.error-message').textContent"

# 6. Any relevant network failures?
npx playwright trace requests --failed test-results/my-test/trace.zip
# 7. Any relevant network failures?
npx playwright trace requests --failed

# 7. Any console errors?
npx playwright trace console --errors-only test-results/my-test/trace.zip
# 8. Any console errors?
npx playwright trace console --errors-only
```
30 changes: 30 additions & 0 deletions packages/playwright-core/src/tools/trace/installSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* eslint-disable no-console */

import fs from 'fs';
import path from 'path';

export async function installSkill() {
const cwd = process.cwd();
const skillSource = path.join(__dirname, 'SKILL.md');
const destDir = path.join(cwd, '.claude', 'playwright-trace');
await fs.promises.mkdir(destDir, { recursive: true });
const destFile = path.join(destDir, 'SKILL.md');
await fs.promises.copyFile(skillSource, destFile);
console.log(`✅ Skill installed to \`${path.relative(cwd, destFile)}\`.`);
}
155 changes: 155 additions & 0 deletions packages/playwright-core/src/tools/trace/traceActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* eslint-disable no-console */

import { buildActionTree } from '../../utils/isomorphic/trace/traceModel';
import { asLocatorDescription } from '../../utils/isomorphic/locatorGenerators';
import { loadTrace, formatTimestamp, actionTitle } from './traceUtils';
import { msToString } from '../../utils/isomorphic/formatUtils';

import type { ActionTraceEventInContext } from '@isomorphic/trace/traceModel';
import type { Language } from '@isomorphic/locatorGenerators';

export async function traceActions(options: { grep?: string, errorsOnly?: boolean }) {
const trace = await loadTrace();
const actions = filterActions(trace.model.actions, options);

// Tree view
const { rootItem } = buildActionTree(actions);
console.log(` ${'#'.padStart(4)} ${'Time'.padEnd(9)} ${'Action'.padEnd(55)} ${'Duration'.padStart(8)}`);
console.log(` ${'─'.repeat(4)} ${'─'.repeat(9)} ${'─'.repeat(55)} ${'─'.repeat(8)}`);
const visit = (item: ReturnType<typeof buildActionTree>['rootItem'], indent: string) => {
const action = item.action;
const ordinal = trace.callIdToOrdinal.get(action.callId) ?? '?';
const ts = formatTimestamp(action.startTime, trace.model.startTime);
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'running';
const title = actionTitle(action as ActionTraceEventInContext);
const locator = actionLocator(action as ActionTraceEventInContext);
const error = action.error ? ' ✗' : '';
const prefix = ` ${(ordinal + '.').padStart(4)} ${ts} ${indent}`;
console.log(`${prefix}${title.padEnd(Math.max(1, 55 - indent.length))} ${duration.padStart(8)}${error}`);
if (locator)
console.log(`${' '.repeat(prefix.length)}${locator}`);
for (const child of item.children)
visit(child, indent + ' ');
};
for (const child of rootItem.children)
visit(child, '');
}

function filterActions(actions: ActionTraceEventInContext[], options: { grep?: string, errorsOnly?: boolean }): ActionTraceEventInContext[] {
let result = actions.filter(a => a.group !== 'configuration');
if (options.grep) {
const pattern = new RegExp(options.grep, 'i');
result = result.filter(a => pattern.test(actionTitle(a)) || pattern.test(actionLocator(a) || ''));
}
if (options.errorsOnly)
result = result.filter(a => !!a.error);
return result;
}

function actionLocator(action: ActionTraceEventInContext, sdkLanguage?: Language): string | undefined {
return action.params.selector ? asLocatorDescription(sdkLanguage || 'javascript', action.params.selector) : undefined;
}

export async function traceAction(actionId: string) {
const trace = await loadTrace();
const action = trace.resolveActionId(actionId);
if (!action) {
console.error(`Action '${actionId}' not found. Use 'trace actions' to see available action IDs.`);
process.exitCode = 1;
return;
}

const title = actionTitle(action);
console.log(`\n ${title}\n`);

// Time
console.log(' Time');
console.log(` start: ${formatTimestamp(action.startTime, trace.model.startTime)}`);
const duration = action.endTime ? msToString(action.endTime - action.startTime) : (action.error ? 'Timed Out' : 'Running');
console.log(` duration: ${duration}`);

// Parameters
const paramKeys = Object.keys(action.params).filter(name => name !== 'info');
if (paramKeys.length) {
console.log('\n Parameters');
for (const key of paramKeys) {
const value = formatParamValue(action.params[key]);
console.log(` ${key}: ${value}`);
}
}

// Return value
if (action.result) {
console.log('\n Return value');
for (const [key, value] of Object.entries(action.result))
console.log(` ${key}: ${formatParamValue(value)}`);

}

// Error
if (action.error) {
console.log('\n Error');
console.log(` ${action.error.message}`);
}

// Logs
if (action.log.length) {
console.log('\n Log');
for (const entry of action.log) {
const time = entry.time !== -1 ? formatTimestamp(entry.time, trace.model.startTime) : '';
console.log(` ${time.padEnd(12)} ${entry.message}`);
}
}

// Source
if (action.stack?.length) {
console.log('\n Source');
for (const frame of action.stack.slice(0, 5)) {
const file = frame.file.replace(/.*[/\\](.*)/, '$1');
console.log(` ${file}:${frame.line}:${frame.column}`);
}
}

// Snapshots
const snapshots: string[] = [];
if (action.beforeSnapshot)
snapshots.push('before');
if (action.inputSnapshot)
snapshots.push('input');
if (action.afterSnapshot)
snapshots.push('after');
if (snapshots.length) {
console.log('\n Snapshots');
console.log(` available: ${snapshots.join(', ')}`);
console.log(` usage: npx playwright trace snapshot ${actionId} --name <${snapshots.join('|')}>`);
}
console.log('');
}

function formatParamValue(value: any): string {
if (value === undefined || value === null)
return String(value);
if (typeof value === 'string')
return `"${value}"`;
if (typeof value !== 'object')
return String(value);
if (value.guid)
return '<handle>';
return JSON.stringify(value).slice(0, 1000);
}
Loading
Loading