@andrii_kremlovskyi/playwright-traces-reader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/extractors.d.ts +131 -0
- package/dist/extractors.d.ts.map +1 -0
- package/dist/extractors.js +429 -0
- package/dist/extractors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/parseTrace.d.ts +30 -0
- package/dist/parseTrace.d.ts.map +1 -0
- package/dist/parseTrace.js +137 -0
- package/dist/parseTrace.js.map +1 -0
- package/package.json +62 -0
- package/templates/skills/analyze-playwright-traces/SKILL.md +196 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# playwright-traces-reader
|
|
2
|
+
|
|
3
|
+
Parse [Playwright](https://playwright.dev) trace files into structured data — useful for AI agents, custom reporters, and post-run analysis tooling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Extract test steps with timings and errors
|
|
8
|
+
- Extract failed steps with full error messages
|
|
9
|
+
- Extract API and browser network traffic (with resolved request/response bodies)
|
|
10
|
+
- Save screenshots from screencasts
|
|
11
|
+
- Extract full DOM snapshots (before / during / after each action) with back-reference resolution
|
|
12
|
+
- Support for multi-test reports (many SHA1 trace entries in one `data/` directory)
|
|
13
|
+
- GitHub Copilot skill scaffold via `init-skills` CLI command
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @kremlovskyi/playwright-traces-reader
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import {
|
|
25
|
+
prepareTraceDir,
|
|
26
|
+
getFailedTests,
|
|
27
|
+
getNetworkTraffic,
|
|
28
|
+
getTestSteps,
|
|
29
|
+
extractScreenshots,
|
|
30
|
+
getDomSnapshots,
|
|
31
|
+
} from '@kremlovskyi/playwright-traces-reader';
|
|
32
|
+
|
|
33
|
+
// Point at a single extracted trace directory (or a .zip)
|
|
34
|
+
const ctx = await prepareTraceDir('/path/to/playwright-report/data/<sha1>');
|
|
35
|
+
|
|
36
|
+
const failures = await getFailedTests(ctx);
|
|
37
|
+
const traffic = await getNetworkTraffic(ctx);
|
|
38
|
+
const steps = await getTestSteps(ctx);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Multi-test reports
|
|
42
|
+
|
|
43
|
+
A Playwright HTML report stores one trace entry per test inside `playwright-report/data/`. Use `listTraces()` to iterate all of them:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { listTraces, getFailedTests } from '@kremlovskyi/playwright-traces-reader';
|
|
47
|
+
|
|
48
|
+
const traces = await listTraces('/path/to/playwright-report/data');
|
|
49
|
+
|
|
50
|
+
for (const ctx of traces) {
|
|
51
|
+
const failures = await getFailedTests(ctx);
|
|
52
|
+
// ... process per-test results
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Both extracted directories and `.zip` archives are handled automatically. Non-trace files (`.md`, `.png`, etc.) are ignored.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `listTraces(reportDataDir)`
|
|
61
|
+
|
|
62
|
+
Discovers all trace contexts inside a `data/` directory. Returns `TraceContext[]`.
|
|
63
|
+
|
|
64
|
+
### `prepareTraceDir(tracePath)`
|
|
65
|
+
|
|
66
|
+
Takes a single path (extracted directory or `.zip`) and returns a `TraceContext`.
|
|
67
|
+
|
|
68
|
+
### `getTestSteps(ctx)`
|
|
69
|
+
|
|
70
|
+
Returns the full step tree from `test.trace` as `TestStep[]`. Each step has:
|
|
71
|
+
|
|
72
|
+
| Field | Type | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `callId` | `string` | Unique step identifier |
|
|
75
|
+
| `title` | `string` | Human-readable step name |
|
|
76
|
+
| `method` | `string \| undefined` | API method if applicable |
|
|
77
|
+
| `startTime` | `number` | Unix ms timestamp |
|
|
78
|
+
| `endTime` | `number \| null` | Unix ms timestamp |
|
|
79
|
+
| `durationMs` | `number \| null` | Wall-clock duration |
|
|
80
|
+
| `error` | `TraceError \| null` | Error details if the step failed |
|
|
81
|
+
| `children` | `TestStep[]` | Nested child steps |
|
|
82
|
+
|
|
83
|
+
### `getFailedTests(ctx)`
|
|
84
|
+
|
|
85
|
+
Walks the step tree and returns a flat `FailedStep[]` for every step that has an `error`. Useful as a quick failure summary.
|
|
86
|
+
|
|
87
|
+
### `getNetworkTraffic(ctx)`
|
|
88
|
+
|
|
89
|
+
Returns `NetworkEntry[]` from all `*.network` trace files. Each entry includes:
|
|
90
|
+
|
|
91
|
+
| Field | Type | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `url` | `string` | Request URL |
|
|
94
|
+
| `method` | `string` | HTTP method |
|
|
95
|
+
| `status` | `number` | HTTP response status |
|
|
96
|
+
| `source` | `'api' \| 'browser'` | `'api'` = Playwright `APIRequestContext`; `'browser'` = XHR / navigation |
|
|
97
|
+
| `requestBody` | `string \| null` | Resolved request body |
|
|
98
|
+
| `responseBody` | `string \| null` | Resolved response body (binary → `[binary: ...]` placeholder) |
|
|
99
|
+
| `contentType` | `string \| null` | Response content-type |
|
|
100
|
+
| `pageref` | `string \| undefined` | Browser page ID (browser traffic only) |
|
|
101
|
+
|
|
102
|
+
### `extractScreenshots(ctx, outDir)`
|
|
103
|
+
|
|
104
|
+
Writes screencast frames from all `[N]-trace.trace` files to `outDir` as numbered `.jpeg` files. Returns `Screenshot[]` with `savedPath`, `timestamp`, `pageId`, `width`, and `height`.
|
|
105
|
+
|
|
106
|
+
### `getDomSnapshots(ctx)`
|
|
107
|
+
|
|
108
|
+
Returns `ActionDomSnapshots[]` — one entry per browser action that has DOM snapshots. Each entry groups three phases:
|
|
109
|
+
|
|
110
|
+
| Field | Type | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `callId` | `string` | Action identifier |
|
|
113
|
+
| `before` | `DomSnapshot \| null` | DOM before the action |
|
|
114
|
+
| `action` | `DomSnapshot \| null` | DOM during the action (mid-interaction) |
|
|
115
|
+
| `after` | `DomSnapshot \| null` | DOM after the action completed |
|
|
116
|
+
|
|
117
|
+
Each `DomSnapshot` contains:
|
|
118
|
+
|
|
119
|
+
| Field | Type | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `html` | `string` | Full serialized HTML (back-references resolved, `<script>` stripped) |
|
|
122
|
+
| `phase` | `'before' \| 'action' \| 'after'` | Snapshot phase |
|
|
123
|
+
| `frameUrl` | `string` | URL of the frame at snapshot time |
|
|
124
|
+
| `targetElement` | `string \| null` | `callId` of the action that targeted an element, or `null` |
|
|
125
|
+
| `viewport` | `{ width: number; height: number }` | Viewport dimensions |
|
|
126
|
+
| `timestamp` | `number` | Unix ms timestamp |
|
|
127
|
+
|
|
128
|
+
### `getResourceBuffer(ctx, sha1)`
|
|
129
|
+
|
|
130
|
+
Low-level helper. Resolves a SHA1 filename to a raw `Buffer` from `resources/`. Returns `null` if not found.
|
|
131
|
+
|
|
132
|
+
### `readNdjson<T>(filePath)`
|
|
133
|
+
|
|
134
|
+
Low-level async generator that streams and parses an NDJSON file line by line. Silently skips malformed lines.
|
|
135
|
+
|
|
136
|
+
## GitHub Copilot Skill
|
|
137
|
+
|
|
138
|
+
Install a ready-made GitHub Copilot skill scaffold into your project:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npx @kremlovskyi/playwright-traces-reader init-skills
|
|
142
|
+
# or into a custom target directory:
|
|
143
|
+
npx @kremlovskyi/playwright-traces-reader init-skills ./my-project
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This copies a `SKILL.md` template to `.github/skills/analyze-playwright-traces/SKILL.md` with code examples for all extractor functions. Once in place, GitHub Copilot will use the skill automatically when answering questions about your Playwright test runs.
|
|
147
|
+
|
|
148
|
+
## Trace Format
|
|
149
|
+
|
|
150
|
+
Playwright HTML reports store traces in `playwright-report/data/<sha1>/`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
<sha1>/
|
|
154
|
+
├── test.trace ← step tree (getTestSteps / getFailedTests)
|
|
155
|
+
├── 0-trace.trace ← browser actions, screenshots, DOM snapshots
|
|
156
|
+
├── 0-trace.network ← network HAR entries
|
|
157
|
+
└── resources/ ← binary blobs (bodies, images) addressed by SHA1
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
All trace files use **Newline-Delimited JSON (NDJSON)**.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const [, , command, ...args] = process.argv;
|
|
40
|
+
async function initSkills(targetDir) {
|
|
41
|
+
const skillDir = path.join(targetDir, '.github', 'skills', 'analyze-playwright-traces');
|
|
42
|
+
await fs.promises.mkdir(skillDir, { recursive: true });
|
|
43
|
+
// Copy the template SKILL.md from this package
|
|
44
|
+
const templatePath = path.resolve(__dirname, '..', 'templates', 'skills', 'analyze-playwright-traces', 'SKILL.md');
|
|
45
|
+
const destPath = path.join(skillDir, 'SKILL.md');
|
|
46
|
+
if (!fs.existsSync(templatePath)) {
|
|
47
|
+
console.error(`Template not found at ${templatePath}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
await fs.promises.copyFile(templatePath, destPath);
|
|
51
|
+
console.log(`✔ Skill scaffolded at ${destPath}`);
|
|
52
|
+
}
|
|
53
|
+
async function main() {
|
|
54
|
+
if (command === 'init-skills') {
|
|
55
|
+
const targetDir = args[0] ?? process.cwd();
|
|
56
|
+
await initSkills(targetDir);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(`
|
|
60
|
+
playwright-traces-reader — CLI for parsing Playwright trace files
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
npx playwright-traces-reader init-skills [targetDir]
|
|
64
|
+
Scaffolds the GitHub Copilot skill template into <targetDir>/.github/skills/analyze-playwright-traces/SKILL.md
|
|
65
|
+
Defaults targetDir to the current working directory.
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
main().catch(err => {
|
|
69
|
+
console.error(err);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,uCAAyB;AACzB,2CAA6B;AAE7B,MAAM,CAAC,EAAE,AAAD,EAAG,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;AAE5C,KAAK,UAAU,UAAU,CAAC,SAAiB;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,CAAC,CAAC;IACxF,MAAM,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvD,+CAA+C;IAC/C,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,2BAA2B,EAAE,UAAU,CAAC,CAAC;IACnH,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAEjD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,yBAAyB,YAAY,EAAE,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAC3C,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC;;;;;;;CAOb,CAAC,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type TraceContext } from './parseTrace';
|
|
2
|
+
export interface TestStep {
|
|
3
|
+
callId: string;
|
|
4
|
+
parentId: string | null;
|
|
5
|
+
title: string;
|
|
6
|
+
method: string;
|
|
7
|
+
startTime: number;
|
|
8
|
+
endTime: number | null;
|
|
9
|
+
durationMs: number | null;
|
|
10
|
+
error: TraceError | null;
|
|
11
|
+
children: TestStep[];
|
|
12
|
+
}
|
|
13
|
+
export interface TraceError {
|
|
14
|
+
name: string;
|
|
15
|
+
message: string;
|
|
16
|
+
stack?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface FailedStep {
|
|
19
|
+
callId: string;
|
|
20
|
+
title: string;
|
|
21
|
+
error: TraceError;
|
|
22
|
+
durationMs: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface NetworkEntry {
|
|
25
|
+
source: 'browser' | 'api';
|
|
26
|
+
method: string;
|
|
27
|
+
url: string;
|
|
28
|
+
status: number;
|
|
29
|
+
statusText: string;
|
|
30
|
+
requestHeaders: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}>;
|
|
34
|
+
responseHeaders: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}>;
|
|
38
|
+
requestBody: string | null;
|
|
39
|
+
responseBody: string | null;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
startedDateTime: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Screenshot {
|
|
45
|
+
sha1: string;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
pageId: string;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
savedPath: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reconstructs the test step tree from a test.trace file.
|
|
54
|
+
* Returns top-level steps (roots), each with nested children.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getTestSteps(traceContext: TraceContext): Promise<TestStep[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Scans test.trace for steps that have errors.
|
|
59
|
+
* Returns a flat list of all steps (at any nesting level) that failed,
|
|
60
|
+
* with the error details and duration.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getFailedTests(traceContext: TraceContext): Promise<FailedStep[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Reads all *.network files in the trace directory and returns structured
|
|
65
|
+
* network entries. Each entry is tagged as 'browser' (has pageref) or
|
|
66
|
+
* 'api' (has _apiRequest, no pageref). Response and request bodies are
|
|
67
|
+
* resolved from the resources/ directory when a _sha1 reference exists.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getNetworkTraffic(traceContext: TraceContext): Promise<NetworkEntry[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Finds all screencast-frame entries across all *-trace.trace files,
|
|
72
|
+
* copies the referenced JPEG/PNG blobs from resources/ into outDir as
|
|
73
|
+
* numbered files, and returns metadata including the saved file path.
|
|
74
|
+
*/
|
|
75
|
+
export declare function extractScreenshots(traceContext: TraceContext, outDir: string): Promise<Screenshot[]>;
|
|
76
|
+
/**
|
|
77
|
+
* A single serialized DOM snapshot for one phase of one action.
|
|
78
|
+
*
|
|
79
|
+
* `phase`:
|
|
80
|
+
* - `"before"` — DOM state before the action started (default recorded point)
|
|
81
|
+
* - `"action"` — DOM state during the action (corresponds to Trace Viewer's "Action" tab)
|
|
82
|
+
* - `"after"` — DOM state after the action completed
|
|
83
|
+
*
|
|
84
|
+
* `html` is the snapshot serialized to an HTML string. Back-references inside
|
|
85
|
+
* Playwright's compact snapshot format (`[[offset, nodeIdx]]`) are resolved using
|
|
86
|
+
* the per-frame snapshot history: `offset` is how many snapshots to go back (from
|
|
87
|
+
* the current snapshot's index) within this frame's history, and `nodeIdx` is the
|
|
88
|
+
* index in the post-order DFS traversal of that historical snapshot.
|
|
89
|
+
*
|
|
90
|
+
* `targetElement` is the value of the `__playwright_target__` attribute on
|
|
91
|
+
* the interacted element (the `callId` of the action that targeted it), if any.
|
|
92
|
+
*/
|
|
93
|
+
export interface DomSnapshot {
|
|
94
|
+
callId: string;
|
|
95
|
+
phase: 'before' | 'action' | 'after';
|
|
96
|
+
snapshotName: string;
|
|
97
|
+
frameId: string;
|
|
98
|
+
frameUrl: string;
|
|
99
|
+
pageId: string;
|
|
100
|
+
timestamp: number;
|
|
101
|
+
viewport: {
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
};
|
|
105
|
+
html: string;
|
|
106
|
+
targetElement: string | null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A complete set of DOM snapshots for a single browser action, grouping
|
|
110
|
+
* the before/action/after phases together.
|
|
111
|
+
*/
|
|
112
|
+
export interface ActionDomSnapshots {
|
|
113
|
+
callId: string;
|
|
114
|
+
before: DomSnapshot | null;
|
|
115
|
+
action: DomSnapshot | null;
|
|
116
|
+
after: DomSnapshot | null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extracts DOM snapshots from all browser context trace files (`[N]-trace.trace`
|
|
120
|
+
* where the context is a browser context, i.e. has frame-snapshot entries).
|
|
121
|
+
*
|
|
122
|
+
* Returns one `ActionDomSnapshots` per unique `callId` found, with all three
|
|
123
|
+
* phases (`before`, `action`, `after`) populated when available.
|
|
124
|
+
*
|
|
125
|
+
* Back-references in Playwright's compact snapshot format are resolved using
|
|
126
|
+
* the per-frame snapshot history: `[[offset, nodeIdx]]` means "go back `offset`
|
|
127
|
+
* snapshots in this frame's history, find the `nodeIdx`-th node in post-order
|
|
128
|
+
* DFS of that snapshot".
|
|
129
|
+
*/
|
|
130
|
+
export declare function getDomSnapshots(traceContext: TraceContext): Promise<ActionDomSnapshots[]>;
|
|
131
|
+
//# sourceMappingURL=extractors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractors.d.ts","sourceRoot":"","sources":["../src/extractors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiC,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAIhF,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,QAAQ,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,GAAG,KAAK,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD,eAAe,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAgED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA2ClF;AAID;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBtF;AAID;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA+D3F;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAmCvB;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3B;AAuID;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAkF/F"}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getTestSteps = getTestSteps;
|
|
37
|
+
exports.getFailedTests = getFailedTests;
|
|
38
|
+
exports.getNetworkTraffic = getNetworkTraffic;
|
|
39
|
+
exports.extractScreenshots = extractScreenshots;
|
|
40
|
+
exports.getDomSnapshots = getDomSnapshots;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const parseTrace_1 = require("./parseTrace");
|
|
44
|
+
// ---------- getTestSteps ----------
|
|
45
|
+
/**
|
|
46
|
+
* Reconstructs the test step tree from a test.trace file.
|
|
47
|
+
* Returns top-level steps (roots), each with nested children.
|
|
48
|
+
*/
|
|
49
|
+
async function getTestSteps(traceContext) {
|
|
50
|
+
const testTracePath = path.join(traceContext.traceDir, 'test.trace');
|
|
51
|
+
const stepMap = new Map();
|
|
52
|
+
const roots = [];
|
|
53
|
+
for await (const event of (0, parseTrace_1.readNdjson)(testTracePath)) {
|
|
54
|
+
if (event.type === 'before') {
|
|
55
|
+
const step = {
|
|
56
|
+
callId: event.callId,
|
|
57
|
+
parentId: event.parentId ?? null,
|
|
58
|
+
title: event.title,
|
|
59
|
+
method: event.method,
|
|
60
|
+
startTime: event.startTime,
|
|
61
|
+
endTime: null,
|
|
62
|
+
durationMs: null,
|
|
63
|
+
error: null,
|
|
64
|
+
children: [],
|
|
65
|
+
};
|
|
66
|
+
stepMap.set(event.callId, step);
|
|
67
|
+
if (event.parentId) {
|
|
68
|
+
const parent = stepMap.get(event.parentId);
|
|
69
|
+
if (parent) {
|
|
70
|
+
parent.children.push(step);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
roots.push(step);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
roots.push(step);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (event.type === 'after') {
|
|
81
|
+
const step = stepMap.get(event.callId);
|
|
82
|
+
if (step) {
|
|
83
|
+
step.endTime = event.endTime;
|
|
84
|
+
step.durationMs = event.endTime - step.startTime;
|
|
85
|
+
if (event.error) {
|
|
86
|
+
step.error = event.error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return roots;
|
|
92
|
+
}
|
|
93
|
+
// ---------- getFailedTests ----------
|
|
94
|
+
/**
|
|
95
|
+
* Scans test.trace for steps that have errors.
|
|
96
|
+
* Returns a flat list of all steps (at any nesting level) that failed,
|
|
97
|
+
* with the error details and duration.
|
|
98
|
+
*/
|
|
99
|
+
async function getFailedTests(traceContext) {
|
|
100
|
+
const roots = await getTestSteps(traceContext);
|
|
101
|
+
const failed = [];
|
|
102
|
+
function collect(steps) {
|
|
103
|
+
for (const step of steps) {
|
|
104
|
+
if (step.error) {
|
|
105
|
+
failed.push({
|
|
106
|
+
callId: step.callId,
|
|
107
|
+
title: step.title,
|
|
108
|
+
error: step.error,
|
|
109
|
+
durationMs: step.durationMs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
collect(step.children);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
collect(roots);
|
|
116
|
+
return failed;
|
|
117
|
+
}
|
|
118
|
+
// ---------- getNetworkTraffic ----------
|
|
119
|
+
/**
|
|
120
|
+
* Reads all *.network files in the trace directory and returns structured
|
|
121
|
+
* network entries. Each entry is tagged as 'browser' (has pageref) or
|
|
122
|
+
* 'api' (has _apiRequest, no pageref). Response and request bodies are
|
|
123
|
+
* resolved from the resources/ directory when a _sha1 reference exists.
|
|
124
|
+
*/
|
|
125
|
+
async function getNetworkTraffic(traceContext) {
|
|
126
|
+
const entries = [];
|
|
127
|
+
const files = await fs.promises.readdir(traceContext.traceDir);
|
|
128
|
+
const networkFiles = files.filter(f => f.endsWith('.network')).sort();
|
|
129
|
+
for (const networkFile of networkFiles) {
|
|
130
|
+
const filePath = path.join(traceContext.traceDir, networkFile);
|
|
131
|
+
for await (const event of (0, parseTrace_1.readNdjson)(filePath)) {
|
|
132
|
+
if (event.type !== 'resource-snapshot')
|
|
133
|
+
continue;
|
|
134
|
+
const snap = event.snapshot;
|
|
135
|
+
const isBrowser = Boolean(snap.pageref);
|
|
136
|
+
const source = isBrowser ? 'browser' : 'api';
|
|
137
|
+
// Resolve request body
|
|
138
|
+
let requestBody = null;
|
|
139
|
+
const postData = snap.request.postData;
|
|
140
|
+
if (postData) {
|
|
141
|
+
if (postData.text) {
|
|
142
|
+
requestBody = postData.text;
|
|
143
|
+
}
|
|
144
|
+
else if (postData._sha1) {
|
|
145
|
+
const sha1 = postData._sha1.replace(/\.bin$/, '');
|
|
146
|
+
const buf = await (0, parseTrace_1.getResourceBuffer)(traceContext, postData._sha1) ??
|
|
147
|
+
await (0, parseTrace_1.getResourceBuffer)(traceContext, sha1);
|
|
148
|
+
if (buf)
|
|
149
|
+
requestBody = buf.toString('utf8');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Resolve response body
|
|
153
|
+
let responseBody = null;
|
|
154
|
+
const content = snap.response.content;
|
|
155
|
+
if (content._sha1) {
|
|
156
|
+
const buf = await (0, parseTrace_1.getResourceBuffer)(traceContext, content._sha1);
|
|
157
|
+
if (buf) {
|
|
158
|
+
if (content.mimeType.includes('json') || content.mimeType.includes('text')) {
|
|
159
|
+
responseBody = buf.toString('utf8');
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
responseBody = `[binary: ${content.mimeType}, ${buf.length} bytes]`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (content.text) {
|
|
167
|
+
responseBody = content.text;
|
|
168
|
+
}
|
|
169
|
+
entries.push({
|
|
170
|
+
source,
|
|
171
|
+
method: snap.request.method,
|
|
172
|
+
url: snap.request.url,
|
|
173
|
+
status: snap.response.status,
|
|
174
|
+
statusText: snap.response.statusText,
|
|
175
|
+
requestHeaders: snap.request.headers,
|
|
176
|
+
responseHeaders: snap.response.headers,
|
|
177
|
+
requestBody,
|
|
178
|
+
responseBody,
|
|
179
|
+
mimeType: content.mimeType,
|
|
180
|
+
startedDateTime: snap.startedDateTime,
|
|
181
|
+
durationMs: snap.time,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return entries;
|
|
186
|
+
}
|
|
187
|
+
// ---------- extractScreenshots ----------
|
|
188
|
+
/**
|
|
189
|
+
* Finds all screencast-frame entries across all *-trace.trace files,
|
|
190
|
+
* copies the referenced JPEG/PNG blobs from resources/ into outDir as
|
|
191
|
+
* numbered files, and returns metadata including the saved file path.
|
|
192
|
+
*/
|
|
193
|
+
async function extractScreenshots(traceContext, outDir) {
|
|
194
|
+
const screenshots = [];
|
|
195
|
+
await fs.promises.mkdir(outDir, { recursive: true });
|
|
196
|
+
const files = await fs.promises.readdir(traceContext.traceDir);
|
|
197
|
+
const traceFiles = files.filter(f => f.endsWith('.trace') && f !== 'test.trace').sort();
|
|
198
|
+
let index = 0;
|
|
199
|
+
for (const traceFile of traceFiles) {
|
|
200
|
+
const filePath = path.join(traceContext.traceDir, traceFile);
|
|
201
|
+
for await (const event of (0, parseTrace_1.readNdjson)(filePath)) {
|
|
202
|
+
if (event.type !== 'screencast-frame')
|
|
203
|
+
continue;
|
|
204
|
+
const buf = await (0, parseTrace_1.getResourceBuffer)(traceContext, event.sha1);
|
|
205
|
+
if (!buf)
|
|
206
|
+
continue;
|
|
207
|
+
const ext = event.sha1.endsWith('.jpeg') ? 'jpeg' : 'png';
|
|
208
|
+
const outFileName = `screenshot-${String(index).padStart(4, '0')}.${ext}`;
|
|
209
|
+
const savedPath = path.join(outDir, outFileName);
|
|
210
|
+
await fs.promises.writeFile(savedPath, buf);
|
|
211
|
+
screenshots.push({
|
|
212
|
+
sha1: event.sha1,
|
|
213
|
+
timestamp: event.timestamp,
|
|
214
|
+
pageId: event.pageId,
|
|
215
|
+
width: event.width,
|
|
216
|
+
height: event.height,
|
|
217
|
+
savedPath,
|
|
218
|
+
});
|
|
219
|
+
index++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return screenshots;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Builds the post-order DFS node list for a snapshot tree (mirroring
|
|
226
|
+
* Playwright's `snapshotNodes()` in snapshotRenderer.ts). Only "real"
|
|
227
|
+
* element/text nodes are added — subtree refs are not in the list.
|
|
228
|
+
* This list is used for back-reference resolution.
|
|
229
|
+
*
|
|
230
|
+
* Results are memoized in `cache` (keyed by the html object reference) to avoid
|
|
231
|
+
* recomputing the same large trees when many back-references point to the same
|
|
232
|
+
* historical snapshot.
|
|
233
|
+
*/
|
|
234
|
+
function buildSnapshotNodeList(html, cache) {
|
|
235
|
+
const cached = cache.get(html);
|
|
236
|
+
if (cached)
|
|
237
|
+
return cached;
|
|
238
|
+
const nodes = [];
|
|
239
|
+
function visit(n) {
|
|
240
|
+
if (typeof n === 'string') {
|
|
241
|
+
nodes.push(n);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!Array.isArray(n))
|
|
245
|
+
return;
|
|
246
|
+
// Subtree ref: [[offset, idx]] — skip (not a real node)
|
|
247
|
+
if (Array.isArray(n[0]))
|
|
248
|
+
return;
|
|
249
|
+
// Element: [tagName, attrs, ...children] — recurse children first (post-order)
|
|
250
|
+
const [, , ...children] = n;
|
|
251
|
+
for (const child of children)
|
|
252
|
+
visit(child);
|
|
253
|
+
nodes.push(n);
|
|
254
|
+
}
|
|
255
|
+
visit(html);
|
|
256
|
+
cache.set(html, nodes);
|
|
257
|
+
return nodes;
|
|
258
|
+
}
|
|
259
|
+
const VOID_TAGS = new Set([
|
|
260
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
261
|
+
'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
|
|
262
|
+
]);
|
|
263
|
+
/**
|
|
264
|
+
* Resolves a snapshot node tree into an HTML string.
|
|
265
|
+
*
|
|
266
|
+
* `snapshotIndex` is the 0-based index of the current snapshot in `frameHistory`.
|
|
267
|
+
* `frameHistory` is the ordered list of all raw snapshot HTMLs seen so far for
|
|
268
|
+
* this frame (current snapshot is already appended before this call).
|
|
269
|
+
* `dfsCache` is a memoization cache shared across the entire trace-file pass to
|
|
270
|
+
* avoid rebuilding the same DFS node lists repeatedly.
|
|
271
|
+
*
|
|
272
|
+
* Back-references `[[offset, nodeIdx]]` are resolved as:
|
|
273
|
+
* refIndex = snapshotIndex - offset
|
|
274
|
+
* node = snapshotNodes(frameHistory[refIndex])[nodeIdx]
|
|
275
|
+
*/
|
|
276
|
+
function resolveSnapshotNode(node, snapshotIndex, frameHistory, dfsCache) {
|
|
277
|
+
if (typeof node === 'string') {
|
|
278
|
+
return escapeHtml(node);
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(node))
|
|
281
|
+
return '';
|
|
282
|
+
// Subtree reference: [[offset, nodeIdx]]
|
|
283
|
+
if (Array.isArray(node[0]) &&
|
|
284
|
+
node[0].length === 2 &&
|
|
285
|
+
typeof node[0][0] === 'number' &&
|
|
286
|
+
typeof node[0][1] === 'number') {
|
|
287
|
+
const [offset, nodeIdx] = node[0];
|
|
288
|
+
const refIndex = snapshotIndex - offset;
|
|
289
|
+
if (refIndex >= 0 && refIndex < frameHistory.length) {
|
|
290
|
+
const refHtml = frameHistory[refIndex];
|
|
291
|
+
const nodes = buildSnapshotNodeList(refHtml, dfsCache);
|
|
292
|
+
if (nodeIdx >= 0 && nodeIdx < nodes.length) {
|
|
293
|
+
return resolveSnapshotNode(nodes[nodeIdx], refIndex, frameHistory, dfsCache);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return `<!-- ref: [${offset}, ${nodeIdx}] unresolved -->`;
|
|
297
|
+
}
|
|
298
|
+
// Element node: [tagName, attrs, ...children]
|
|
299
|
+
const [tagName, attrs, ...children] = node;
|
|
300
|
+
if (typeof tagName !== 'string')
|
|
301
|
+
return '';
|
|
302
|
+
const tag = tagName.toLowerCase();
|
|
303
|
+
if (tag === 'script')
|
|
304
|
+
return ''; // not useful for AI analysis
|
|
305
|
+
const attrStr = attrs && typeof attrs === 'object' && !Array.isArray(attrs)
|
|
306
|
+
? Object.entries(attrs)
|
|
307
|
+
.filter(([k]) => !k.startsWith('__playwright'))
|
|
308
|
+
.map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`)
|
|
309
|
+
.join('')
|
|
310
|
+
: '';
|
|
311
|
+
if (VOID_TAGS.has(tag)) {
|
|
312
|
+
return `<${tag}${attrStr}>`;
|
|
313
|
+
}
|
|
314
|
+
const inner = children.map(c => resolveSnapshotNode(c, snapshotIndex, frameHistory, dfsCache)).join('');
|
|
315
|
+
return `<${tag}${attrStr}>${inner}</${tag}>`;
|
|
316
|
+
}
|
|
317
|
+
function escapeHtml(s) {
|
|
318
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
319
|
+
}
|
|
320
|
+
function escapeAttr(s) {
|
|
321
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Extracts DOM snapshots from all browser context trace files (`[N]-trace.trace`
|
|
325
|
+
* where the context is a browser context, i.e. has frame-snapshot entries).
|
|
326
|
+
*
|
|
327
|
+
* Returns one `ActionDomSnapshots` per unique `callId` found, with all three
|
|
328
|
+
* phases (`before`, `action`, `after`) populated when available.
|
|
329
|
+
*
|
|
330
|
+
* Back-references in Playwright's compact snapshot format are resolved using
|
|
331
|
+
* the per-frame snapshot history: `[[offset, nodeIdx]]` means "go back `offset`
|
|
332
|
+
* snapshots in this frame's history, find the `nodeIdx`-th node in post-order
|
|
333
|
+
* DFS of that snapshot".
|
|
334
|
+
*/
|
|
335
|
+
async function getDomSnapshots(traceContext) {
|
|
336
|
+
const files = await fs.promises.readdir(traceContext.traceDir);
|
|
337
|
+
const traceFiles = files.filter(f => f.endsWith('.trace') && f !== 'test.trace').sort();
|
|
338
|
+
// Per-frame snapshot history: frameId → ordered list of raw html nodes
|
|
339
|
+
const frameHistory = new Map();
|
|
340
|
+
// Memoization cache for buildSnapshotNodeList — keyed by html object reference
|
|
341
|
+
const dfsCache = new Map();
|
|
342
|
+
// Collect resolved snapshots keyed by callId → phase
|
|
343
|
+
const rawByCallId = new Map();
|
|
344
|
+
for (const traceFile of traceFiles) {
|
|
345
|
+
const filePath = path.join(traceContext.traceDir, traceFile);
|
|
346
|
+
for await (const event of (0, parseTrace_1.readNdjson)(filePath)) {
|
|
347
|
+
if (event.type !== 'frame-snapshot')
|
|
348
|
+
continue;
|
|
349
|
+
const snap = event.snapshot;
|
|
350
|
+
const { snapshotName, callId, frameId, frameUrl, pageId, viewport, timestamp, html } = snap;
|
|
351
|
+
// Determine phase from snapshotName prefix
|
|
352
|
+
let phase;
|
|
353
|
+
if (snapshotName.startsWith('before@')) {
|
|
354
|
+
phase = 'before';
|
|
355
|
+
}
|
|
356
|
+
else if (snapshotName.startsWith('after@')) {
|
|
357
|
+
phase = 'after';
|
|
358
|
+
}
|
|
359
|
+
else if (snapshotName.startsWith('input@')) {
|
|
360
|
+
phase = 'action';
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
continue; // unknown phase, skip
|
|
364
|
+
}
|
|
365
|
+
// Append to per-frame history BEFORE resolving (so refs can point back to it)
|
|
366
|
+
if (!frameHistory.has(frameId))
|
|
367
|
+
frameHistory.set(frameId, []);
|
|
368
|
+
const history = frameHistory.get(frameId);
|
|
369
|
+
history.push(html);
|
|
370
|
+
const snapshotIndex = history.length - 1;
|
|
371
|
+
// Resolve the full html to a string using the correct ref algorithm
|
|
372
|
+
const resolvedHtml = resolveSnapshotNode(html, snapshotIndex, history, dfsCache);
|
|
373
|
+
// Extract the __playwright_target__ attribute to identify the targeted element
|
|
374
|
+
const targetElement = findPlaywrightTarget(html);
|
|
375
|
+
const domSnapshot = {
|
|
376
|
+
callId,
|
|
377
|
+
phase,
|
|
378
|
+
snapshotName,
|
|
379
|
+
frameId,
|
|
380
|
+
frameUrl,
|
|
381
|
+
pageId,
|
|
382
|
+
timestamp,
|
|
383
|
+
viewport,
|
|
384
|
+
html: resolvedHtml,
|
|
385
|
+
targetElement,
|
|
386
|
+
};
|
|
387
|
+
if (!rawByCallId.has(callId))
|
|
388
|
+
rawByCallId.set(callId, new Map());
|
|
389
|
+
rawByCallId.get(callId).set(phase, domSnapshot);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Assemble into ActionDomSnapshots
|
|
393
|
+
const result = [];
|
|
394
|
+
for (const [callId, phases] of rawByCallId) {
|
|
395
|
+
result.push({
|
|
396
|
+
callId,
|
|
397
|
+
before: phases.get('before') ?? null,
|
|
398
|
+
action: phases.get('action') ?? null,
|
|
399
|
+
after: phases.get('after') ?? null,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
result.sort((a, b) => {
|
|
403
|
+
const ta = (a.before ?? a.action ?? a.after)?.timestamp ?? 0;
|
|
404
|
+
const tb = (b.before ?? b.action ?? b.after)?.timestamp ?? 0;
|
|
405
|
+
return ta - tb;
|
|
406
|
+
});
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
/** Recursively searches the snapshot tree for a node with __playwright_target__ */
|
|
410
|
+
function findPlaywrightTarget(node) {
|
|
411
|
+
if (!Array.isArray(node) || node.length < 2)
|
|
412
|
+
return null;
|
|
413
|
+
const attrs = node[1];
|
|
414
|
+
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
|
|
415
|
+
const target = attrs['__playwright_target__'];
|
|
416
|
+
if (target)
|
|
417
|
+
return target;
|
|
418
|
+
}
|
|
419
|
+
for (let i = 2; i < node.length; i++) {
|
|
420
|
+
const child = node[i];
|
|
421
|
+
if (Array.isArray(child)) {
|
|
422
|
+
const found = findPlaywrightTarget(child);
|
|
423
|
+
if (found)
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=extractors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractors.js","sourceRoot":"","sources":["../src/extractors.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { prepareTraceDir, readNdjson, getResourceBuffer, listTraces } from './parseTrace';
|
|
2
|
+
export type { TraceContext } from './parseTrace';
|
|
3
|
+
export { getTestSteps, getFailedTests, getNetworkTraffic, extractScreenshots, getDomSnapshots, } from './extractors';
|
|
4
|
+
export type { TestStep, TraceError, FailedStep, NetworkEntry, Screenshot, DomSnapshot, ActionDomSnapshots, } from './extractors';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1F,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EACL,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACX,kBAAkB,GACnB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDomSnapshots = exports.extractScreenshots = exports.getNetworkTraffic = exports.getFailedTests = exports.getTestSteps = exports.listTraces = exports.getResourceBuffer = exports.readNdjson = exports.prepareTraceDir = void 0;
|
|
4
|
+
var parseTrace_1 = require("./parseTrace");
|
|
5
|
+
Object.defineProperty(exports, "prepareTraceDir", { enumerable: true, get: function () { return parseTrace_1.prepareTraceDir; } });
|
|
6
|
+
Object.defineProperty(exports, "readNdjson", { enumerable: true, get: function () { return parseTrace_1.readNdjson; } });
|
|
7
|
+
Object.defineProperty(exports, "getResourceBuffer", { enumerable: true, get: function () { return parseTrace_1.getResourceBuffer; } });
|
|
8
|
+
Object.defineProperty(exports, "listTraces", { enumerable: true, get: function () { return parseTrace_1.listTraces; } });
|
|
9
|
+
var extractors_1 = require("./extractors");
|
|
10
|
+
Object.defineProperty(exports, "getTestSteps", { enumerable: true, get: function () { return extractors_1.getTestSteps; } });
|
|
11
|
+
Object.defineProperty(exports, "getFailedTests", { enumerable: true, get: function () { return extractors_1.getFailedTests; } });
|
|
12
|
+
Object.defineProperty(exports, "getNetworkTraffic", { enumerable: true, get: function () { return extractors_1.getNetworkTraffic; } });
|
|
13
|
+
Object.defineProperty(exports, "extractScreenshots", { enumerable: true, get: function () { return extractors_1.extractScreenshots; } });
|
|
14
|
+
Object.defineProperty(exports, "getDomSnapshots", { enumerable: true, get: function () { return extractors_1.getDomSnapshots; } });
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA0F;AAAjF,6GAAA,eAAe,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAAE,wGAAA,UAAU,OAAA;AAGnE,2CAMsB;AALpB,0GAAA,YAAY,OAAA;AACZ,4GAAA,cAAc,OAAA;AACd,+GAAA,iBAAiB,OAAA;AACjB,gHAAA,kBAAkB,OAAA;AAClB,6GAAA,eAAe,OAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface TraceContext {
|
|
2
|
+
traceDir: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Ensures the trace is available as a directory.
|
|
6
|
+
* If a .zip file is provided, it extracts it to a temporary directory.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prepareTraceDir(tracePath: string): Promise<TraceContext>;
|
|
9
|
+
/**
|
|
10
|
+
* Reads a Newline Delimited JSON (NDJSON) file line by line and yields parsed objects.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readNdjson<T = any>(filePath: string): AsyncGenerator<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Returns raw file buffer from the resources/ directory of a trace
|
|
15
|
+
*/
|
|
16
|
+
export declare function getResourceBuffer(traceContext: TraceContext, sha1: string): Promise<Buffer | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Discovers all individual test traces inside a Playwright report data directory.
|
|
19
|
+
*
|
|
20
|
+
* A report's `data/` directory can contain many SHA1-named entries — one per test
|
|
21
|
+
* execution. Each entry is either a directory or a `.zip` archive. Non-trace files
|
|
22
|
+
* (`.md`, `.png`, `.json`, etc.) are ignored.
|
|
23
|
+
*
|
|
24
|
+
* Returns a `TraceContext` for every trace found, with `.zip` archives extracted
|
|
25
|
+
* in-place (same directory, no suffix).
|
|
26
|
+
*
|
|
27
|
+
* @param reportDataDir Path to the `playwright-report/data/` directory.
|
|
28
|
+
*/
|
|
29
|
+
export declare function listTraces(reportDataDir: string): Promise<TraceContext[]>;
|
|
30
|
+
//# sourceMappingURL=parseTrace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseTrace.d.ts","sourceRoot":"","sources":["../src/parseTrace.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgB9E;AAED;;GAEG;AACH,wBAAuB,UAAU,CAAC,CAAC,GAAG,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAmB9E;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA6B/E"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.prepareTraceDir = prepareTraceDir;
|
|
40
|
+
exports.readNdjson = readNdjson;
|
|
41
|
+
exports.getResourceBuffer = getResourceBuffer;
|
|
42
|
+
exports.listTraces = listTraces;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const readline = __importStar(require("readline"));
|
|
46
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
47
|
+
/**
|
|
48
|
+
* Ensures the trace is available as a directory.
|
|
49
|
+
* If a .zip file is provided, it extracts it to a temporary directory.
|
|
50
|
+
*/
|
|
51
|
+
async function prepareTraceDir(tracePath) {
|
|
52
|
+
const stat = await fs.promises.stat(tracePath);
|
|
53
|
+
if (stat.isDirectory()) {
|
|
54
|
+
return { traceDir: tracePath };
|
|
55
|
+
}
|
|
56
|
+
if (tracePath.endsWith('.zip')) {
|
|
57
|
+
const outDir = tracePath.replace(/\.zip$/, '');
|
|
58
|
+
if (!fs.existsSync(outDir)) {
|
|
59
|
+
const zip = new adm_zip_1.default(tracePath);
|
|
60
|
+
zip.extractAllTo(outDir, true);
|
|
61
|
+
}
|
|
62
|
+
return { traceDir: outDir };
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Invalid trace path: ${tracePath}. Must be a directory or .zip file.`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reads a Newline Delimited JSON (NDJSON) file line by line and yields parsed objects.
|
|
68
|
+
*/
|
|
69
|
+
async function* readNdjson(filePath) {
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const fileStream = fs.createReadStream(filePath);
|
|
74
|
+
const rl = readline.createInterface({
|
|
75
|
+
input: fileStream,
|
|
76
|
+
crlfDelay: Infinity
|
|
77
|
+
});
|
|
78
|
+
for await (const line of rl) {
|
|
79
|
+
if (line.trim()) {
|
|
80
|
+
try {
|
|
81
|
+
yield JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// Ignore malformed lines
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns raw file buffer from the resources/ directory of a trace
|
|
91
|
+
*/
|
|
92
|
+
async function getResourceBuffer(traceContext, sha1) {
|
|
93
|
+
const resourcePath = path.join(traceContext.traceDir, 'resources', sha1);
|
|
94
|
+
if (fs.existsSync(resourcePath)) {
|
|
95
|
+
return fs.promises.readFile(resourcePath);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Discovers all individual test traces inside a Playwright report data directory.
|
|
101
|
+
*
|
|
102
|
+
* A report's `data/` directory can contain many SHA1-named entries — one per test
|
|
103
|
+
* execution. Each entry is either a directory or a `.zip` archive. Non-trace files
|
|
104
|
+
* (`.md`, `.png`, `.json`, etc.) are ignored.
|
|
105
|
+
*
|
|
106
|
+
* Returns a `TraceContext` for every trace found, with `.zip` archives extracted
|
|
107
|
+
* in-place (same directory, no suffix).
|
|
108
|
+
*
|
|
109
|
+
* @param reportDataDir Path to the `playwright-report/data/` directory.
|
|
110
|
+
*/
|
|
111
|
+
async function listTraces(reportDataDir) {
|
|
112
|
+
const entries = await fs.promises.readdir(reportDataDir, { withFileTypes: true });
|
|
113
|
+
const tracePaths = [];
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const fullPath = path.join(reportDataDir, entry.name);
|
|
116
|
+
// Resolve symlinks so we can reliably check if it's a directory
|
|
117
|
+
const isDir = entry.isDirectory() ||
|
|
118
|
+
(entry.isSymbolicLink() && (await fs.promises.stat(fullPath)).isDirectory());
|
|
119
|
+
if (isDir) {
|
|
120
|
+
// A directory is a trace if it contains a test.trace file
|
|
121
|
+
const testTracePath = path.join(fullPath, 'test.trace');
|
|
122
|
+
if (fs.existsSync(testTracePath)) {
|
|
123
|
+
tracePaths.push(fullPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (entry.isFile() && entry.name.endsWith('.zip')) {
|
|
127
|
+
const extractedDir = fullPath.replace(/\.zip$/, '');
|
|
128
|
+
// Only include if the extracted form is not already represented as a directory entry
|
|
129
|
+
if (!fs.existsSync(extractedDir)) {
|
|
130
|
+
tracePaths.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
// If the directory already exists (previously extracted), it was already added above
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Promise.all(tracePaths.map(p => prepareTraceDir(p)));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=parseTrace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseTrace.js","sourceRoot":"","sources":["../src/parseTrace.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@andrii_kremlovskyi/playwright-traces-reader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Parse Playwright trace files into structured data for AI agents and tooling",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"playwright-traces-reader": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"test": "jest"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"playwright",
|
|
21
|
+
"traces",
|
|
22
|
+
"github-copilot",
|
|
23
|
+
"llm",
|
|
24
|
+
"skills"
|
|
25
|
+
],
|
|
26
|
+
"author": "Andrii Kremlovskyi",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/adm-zip": "^0.5.8",
|
|
30
|
+
"@types/jest": "^30.0.0",
|
|
31
|
+
"@types/node": "^25.5.0",
|
|
32
|
+
"jest": "^30.3.0",
|
|
33
|
+
"ts-jest": "^29.4.6",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"adm-zip": "^0.5.16"
|
|
39
|
+
},
|
|
40
|
+
"jest": {
|
|
41
|
+
"testEnvironment": "node",
|
|
42
|
+
"testMatch": [
|
|
43
|
+
"**/tests/**/*.test.ts"
|
|
44
|
+
],
|
|
45
|
+
"transform": {
|
|
46
|
+
"^.+\\.tsx?$": [
|
|
47
|
+
"ts-jest",
|
|
48
|
+
{
|
|
49
|
+
"tsconfig": {
|
|
50
|
+
"module": "commonjs",
|
|
51
|
+
"moduleResolution": "node",
|
|
52
|
+
"esModuleInterop": true,
|
|
53
|
+
"types": [
|
|
54
|
+
"node",
|
|
55
|
+
"jest"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: analyze-playwright-traces
|
|
3
|
+
description: Analyze Playwright test traces to find test failures, extract network request/response payloads, identify slow steps, locate UI screenshots, and inspect DOM state before/during/after each browser action.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Analyze Playwright Traces
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks about test failures, API payloads in tests, slow steps, or UI screenshots from Playwright test runs.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
This skill requires the `@kremlovskyi/playwright-traces-reader` npm package in the project:
|
|
13
|
+
```bash
|
|
14
|
+
npm install @kremlovskyi/playwright-traces-reader
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
And a built trace directory under `playwright-report/data/<sha1>/` (or a `.zip` of the same).
|
|
18
|
+
|
|
19
|
+
## How to Use
|
|
20
|
+
|
|
21
|
+
### 1. Find failed tests and error details
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { prepareTraceDir, getFailedTests } from 'playwright-traces-reader';
|
|
25
|
+
|
|
26
|
+
const ctx = await prepareTraceDir('/path/to/playwright-report/data/<sha1>');
|
|
27
|
+
const failures = await getFailedTests(ctx);
|
|
28
|
+
|
|
29
|
+
for (const f of failures) {
|
|
30
|
+
console.log(`FAILED: ${f.title}`);
|
|
31
|
+
console.log(`Error: ${f.error.message}`);
|
|
32
|
+
console.log(`Duration: ${f.durationMs}ms`);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Find slow steps
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { prepareTraceDir, getTestSteps } from 'playwright-traces-reader';
|
|
40
|
+
|
|
41
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
42
|
+
const steps = await getTestSteps(ctx);
|
|
43
|
+
|
|
44
|
+
function flatten(steps: TestStep[], out: TestStep[] = []): TestStep[] {
|
|
45
|
+
for (const s of steps) { out.push(s); flatten(s.children, out); }
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allSteps = flatten(steps);
|
|
50
|
+
const sorted = allSteps
|
|
51
|
+
.filter(s => s.durationMs !== null)
|
|
52
|
+
.sort((a, b) => (b.durationMs ?? 0) - (a.durationMs ?? 0));
|
|
53
|
+
|
|
54
|
+
console.log('Top 5 slowest steps:');
|
|
55
|
+
sorted.slice(0, 5).forEach(s => console.log(` ${s.durationMs}ms — ${s.title}`));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Extract API request/response bodies
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { prepareTraceDir, getNetworkTraffic } from 'playwright-traces-reader';
|
|
62
|
+
|
|
63
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
64
|
+
const traffic = await getNetworkTraffic(ctx);
|
|
65
|
+
|
|
66
|
+
// Filter only API (Node.js APIRequestContext) calls
|
|
67
|
+
const apiCalls = traffic.filter(e => e.source === 'api');
|
|
68
|
+
for (const call of apiCalls) {
|
|
69
|
+
console.log(`${call.method} ${call.url} → ${call.status}`);
|
|
70
|
+
if (call.responseBody) console.log('Response:', call.responseBody.slice(0, 500));
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Extract screenshots for visual inspection
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { prepareTraceDir, extractScreenshots } from 'playwright-traces-reader';
|
|
78
|
+
import * as os from 'os';
|
|
79
|
+
|
|
80
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
81
|
+
const screenshots = await extractScreenshots(ctx, `${os.tmpdir()}/pw-screenshots`);
|
|
82
|
+
|
|
83
|
+
for (const s of screenshots) {
|
|
84
|
+
console.log(`Screenshot at ${s.timestamp}ms: ${s.savedPath}`);
|
|
85
|
+
}
|
|
86
|
+
// Pass savedPath to your image viewer or AI model for visual inspection
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 5. Inspect DOM state before/during/after each action
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { prepareTraceDir, getDomSnapshots } from 'playwright-traces-reader';
|
|
93
|
+
|
|
94
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
95
|
+
const snapshots = await getDomSnapshots(ctx);
|
|
96
|
+
|
|
97
|
+
for (const action of snapshots) {
|
|
98
|
+
// action.before — DOM before the action
|
|
99
|
+
// action.action — DOM during the action (mid-interaction state)
|
|
100
|
+
// action.after — DOM after the action
|
|
101
|
+
const snap = action.after ?? action.before;
|
|
102
|
+
if (!snap) continue;
|
|
103
|
+
|
|
104
|
+
console.log(`callId: ${action.callId} | url: ${snap.frameUrl}`);
|
|
105
|
+
if (snap.targetElement) {
|
|
106
|
+
console.log(` targeted element (callId attr): ${snap.targetElement}`);
|
|
107
|
+
}
|
|
108
|
+
console.log(` DOM snippet: ${snap.html.slice(0, 300)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find the DOM snapshot for a specific action by its callId
|
|
112
|
+
const target = snapshots.find(s => s.callId === 'call@42');
|
|
113
|
+
if (target?.after) {
|
|
114
|
+
console.log('DOM after action 42:', target.after.html);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each `DomSnapshot` includes:
|
|
119
|
+
- `html` — full serialized HTML string (back-references resolved, `<script>` tags stripped)
|
|
120
|
+
- `phase` — `"before"` | `"action"` | `"after"`
|
|
121
|
+
- `frameUrl` — URL of the page frame at snapshot time
|
|
122
|
+
- `targetElement` — `callId` of the action that targeted an element (from `__playwright_target__` attr), or `null`
|
|
123
|
+
- `viewport` — `{ width, height }` at snapshot time
|
|
124
|
+
|
|
125
|
+
## Locating Trace Directories
|
|
126
|
+
|
|
127
|
+
A Playwright report's `data/` directory holds **one SHA1 entry per test**. A test suite run with multiple tests will have multiple entries. Use `listTraces()` to iterate all of them:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { listTraces, getFailedTests } from 'playwright-traces-reader';
|
|
131
|
+
|
|
132
|
+
// Works for 1 test or many — automatically discovers all SHA1 trace entries
|
|
133
|
+
const traces = await listTraces('/path/to/playwright-report/data');
|
|
134
|
+
|
|
135
|
+
for (const ctx of traces) {
|
|
136
|
+
const failures = await getFailedTests(ctx);
|
|
137
|
+
// ... process results per test
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Each entry is either a directory (already extracted) or a `.zip` (auto-extracted). Non-trace files in `data/` (`.md`, `.png`, etc.) are ignored automatically.
|
|
142
|
+
|
|
143
|
+
## Low-Level Utilities
|
|
144
|
+
|
|
145
|
+
### `prepareTraceDir(tracePath)`
|
|
146
|
+
|
|
147
|
+
Takes a single path (directory or `.zip`) and returns a `TraceContext`. Use this when you have a specific trace path rather than a whole `data/` directory.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { prepareTraceDir } from '@kremlovskyi/playwright-traces-reader';
|
|
151
|
+
|
|
152
|
+
const ctx = await prepareTraceDir('/path/to/playwright-report/data/<sha1>');
|
|
153
|
+
// or a zip:
|
|
154
|
+
const ctx = await prepareTraceDir('/path/to/playwright-report/data/<sha1>.zip');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `getResourceBuffer(ctx, sha1)`
|
|
158
|
+
|
|
159
|
+
Resolves a SHA1 filename to its raw `Buffer` from the trace's `resources/` directory. Returns `null` if the blob is not found. Useful when you need the raw bytes of a request/response body or a screenshot blob.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { prepareTraceDir, getResourceBuffer } from '@kremlovskyi/playwright-traces-reader';
|
|
163
|
+
|
|
164
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
165
|
+
const buf = await getResourceBuffer(ctx, 'a1b2c3d4...');
|
|
166
|
+
if (buf) {
|
|
167
|
+
// e.g. write to disk, pass to an image model, etc.
|
|
168
|
+
console.log(`blob size: ${buf.byteLength} bytes`);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `readNdjson<T>(filePath)`
|
|
173
|
+
|
|
174
|
+
Async generator that streams and parses an NDJSON file line by line. Silently skips malformed lines. Useful for reading raw trace events when the built-in extractors don't cover your use case.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { prepareTraceDir, readNdjson } from '@kremlovskyi/playwright-traces-reader';
|
|
178
|
+
import * as path from 'path';
|
|
179
|
+
|
|
180
|
+
const ctx = await prepareTraceDir('/path/to/trace-dir');
|
|
181
|
+
const traceFile = path.join(ctx.traceDir, '0-trace.trace');
|
|
182
|
+
|
|
183
|
+
for await (const event of readNdjson<{ type: string }>(traceFile)) {
|
|
184
|
+
if (event.type === 'screencast-frame') {
|
|
185
|
+
console.log('screencast frame:', event);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
- **Failures**: Start with `getFailedTests()` to see the error message and which step failed. The `error.message` contains the full diff for assertion failures.
|
|
193
|
+
- **Network issues**: Use `getNetworkTraffic()` filtering by `source === 'api'` to see all HTTP calls made by the test. Check `status >= 400` for HTTP errors.
|
|
194
|
+
- **Slow tests**: Use `getTestSteps()` and sort by `durationMs` to find bottlenecks.
|
|
195
|
+
- **Visual state**: Use `extractScreenshots()` and look at the last screenshot before a failure to understand the UI state at the time of failure.
|
|
196
|
+
- **DOM inspection**: Use `getDomSnapshots()` to get the full HTML page state at each action. The `after` snapshot is the most useful for debugging — it shows exactly what the page looked like after Playwright performed an action. Combine with `targetElement` to identify which element Playwright interacted with.
|