@pedropaulovc/playwright-core 1.59.0-next → 1.59.0-next.1
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/bin/reinstall_chrome_beta_linux.sh +0 -0
- package/bin/reinstall_chrome_beta_mac.sh +0 -0
- package/bin/reinstall_chrome_stable_linux.sh +0 -0
- package/bin/reinstall_chrome_stable_mac.sh +0 -0
- package/bin/reinstall_msedge_beta_linux.sh +0 -0
- package/bin/reinstall_msedge_beta_mac.sh +0 -0
- package/bin/reinstall_msedge_dev_linux.sh +0 -0
- package/bin/reinstall_msedge_dev_mac.sh +0 -0
- package/bin/reinstall_msedge_stable_linux.sh +0 -0
- package/bin/reinstall_msedge_stable_mac.sh +0 -0
- package/lib/cli/program.js +3 -5
- package/lib/client/page.js +0 -2
- package/lib/server/trace/viewer/traceExporter.js +413 -118
- package/lib/utilsBundle.js +3 -0
- package/lib/utilsBundleImpl/xdg-open +0 -0
- package/package.json +2 -2
- package/types/types.d.ts +327 -0
- package/lib/vite/traceViewer/assets/codeMirrorModule-DwzBH9eL.js +0 -32
- package/lib/vite/traceViewer/assets/defaultSettingsView-CdCX8877.js +0 -266
- package/lib/vite/traceViewer/index.f4OcrOqs.js +0 -2
- package/lib/vite/traceViewer/uiMode.qcahlSup.js +0 -5
|
@@ -34,6 +34,7 @@ module.exports = __toCommonJS(traceExporter_exports);
|
|
|
34
34
|
var import_fs = __toESM(require("fs"));
|
|
35
35
|
var import_path = __toESM(require("path"));
|
|
36
36
|
var import_zipFile = require("../../utils/zipFile");
|
|
37
|
+
var import_stringUtils = require("../../../utils/isomorphic/stringUtils");
|
|
37
38
|
async function exportTraceToMarkdown(traceFile, options) {
|
|
38
39
|
const context = await parseTrace(traceFile);
|
|
39
40
|
const outputDir = options.outputDir;
|
|
@@ -41,16 +42,14 @@ async function exportTraceToMarkdown(traceFile, options) {
|
|
|
41
42
|
const screenshotsDir = import_path.default.join(assetsDir, "screenshots");
|
|
42
43
|
const snapshotsDir = import_path.default.join(assetsDir, "snapshots");
|
|
43
44
|
await import_fs.default.promises.mkdir(outputDir, { recursive: true });
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await import_fs.default.promises.mkdir(snapshotsDir, { recursive: true });
|
|
48
|
-
assetMap = await extractAssets(traceFile, context, outputDir);
|
|
49
|
-
}
|
|
45
|
+
await import_fs.default.promises.mkdir(screenshotsDir, { recursive: true });
|
|
46
|
+
await import_fs.default.promises.mkdir(snapshotsDir, { recursive: true });
|
|
47
|
+
const assetMap = await extractAssets(traceFile, context, outputDir);
|
|
50
48
|
const files = [
|
|
49
|
+
{ name: "README.md", content: generateReadmeMarkdown() },
|
|
51
50
|
{ name: "index.md", content: generateIndexMarkdown(context, traceFile) },
|
|
52
51
|
{ name: "metadata.md", content: generateMetadataMarkdown(context) },
|
|
53
|
-
{ name: "timeline.md", content: generateTimelineMarkdown(context.actions, assetMap) },
|
|
52
|
+
{ name: "timeline.md", content: generateTimelineMarkdown(context.actions, assetMap, buildStepSnapshotMap(context.actions)) },
|
|
54
53
|
{ name: "errors.md", content: generateErrorsMarkdown(context.errors, context.actions) },
|
|
55
54
|
{ name: "console.md", content: generateConsoleMarkdown(context.events) },
|
|
56
55
|
{ name: "network.md", content: generateNetworkMarkdown(context.resources) }
|
|
@@ -72,7 +71,8 @@ async function parseTrace(traceFile) {
|
|
|
72
71
|
errors: [],
|
|
73
72
|
resources: [],
|
|
74
73
|
pages: [],
|
|
75
|
-
snapshots: []
|
|
74
|
+
snapshots: [],
|
|
75
|
+
networkResourceMap: /* @__PURE__ */ new Map()
|
|
76
76
|
};
|
|
77
77
|
const actionMap = /* @__PURE__ */ new Map();
|
|
78
78
|
const pageMap = /* @__PURE__ */ new Map();
|
|
@@ -124,7 +124,11 @@ function processTraceEvent(event, context, actionMap, pageMap) {
|
|
|
124
124
|
log: [],
|
|
125
125
|
stack: event.stack,
|
|
126
126
|
beforeSnapshot: event.beforeSnapshot,
|
|
127
|
-
pageId: event.pageId
|
|
127
|
+
pageId: event.pageId,
|
|
128
|
+
parentId: event.parentId,
|
|
129
|
+
title: event.title,
|
|
130
|
+
group: event.group,
|
|
131
|
+
stepId: event.stepId
|
|
128
132
|
});
|
|
129
133
|
break;
|
|
130
134
|
case "after":
|
|
@@ -162,10 +166,12 @@ function processTraceEvent(event, context, actionMap, pageMap) {
|
|
|
162
166
|
break;
|
|
163
167
|
case "resource-snapshot":
|
|
164
168
|
if (event.snapshot) {
|
|
169
|
+
const url = event.snapshot.request?.url || "";
|
|
170
|
+
const sha1 = event.snapshot.response?.content?._sha1;
|
|
165
171
|
context.resources.push({
|
|
166
172
|
request: {
|
|
167
173
|
method: event.snapshot.request?.method || "GET",
|
|
168
|
-
url
|
|
174
|
+
url
|
|
169
175
|
},
|
|
170
176
|
response: {
|
|
171
177
|
status: event.snapshot.response?.status || 0,
|
|
@@ -173,6 +179,8 @@ function processTraceEvent(event, context, actionMap, pageMap) {
|
|
|
173
179
|
_failureText: event.snapshot.response?._failureText
|
|
174
180
|
}
|
|
175
181
|
});
|
|
182
|
+
if (url && sha1)
|
|
183
|
+
context.networkResourceMap.set(url, sha1);
|
|
176
184
|
}
|
|
177
185
|
break;
|
|
178
186
|
case "screencast-frame":
|
|
@@ -195,7 +203,10 @@ function processTraceEvent(event, context, actionMap, pageMap) {
|
|
|
195
203
|
frameId: event.snapshot.frameId || "",
|
|
196
204
|
frameUrl: event.snapshot.frameUrl || "",
|
|
197
205
|
html: event.snapshot.html,
|
|
198
|
-
timestamp: event.snapshot.timestamp || 0
|
|
206
|
+
timestamp: event.snapshot.timestamp || 0,
|
|
207
|
+
resourceOverrides: event.snapshot.resourceOverrides || [],
|
|
208
|
+
doctype: event.snapshot.doctype,
|
|
209
|
+
viewport: event.snapshot.viewport
|
|
199
210
|
});
|
|
200
211
|
}
|
|
201
212
|
break;
|
|
@@ -205,60 +216,147 @@ async function extractAssets(traceFile, context, outputDir) {
|
|
|
205
216
|
const assetMap = /* @__PURE__ */ new Map();
|
|
206
217
|
const zipFile = new import_zipFile.ZipFile(traceFile);
|
|
207
218
|
const entries = await zipFile.entries();
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
const resourcesDir = import_path.default.join(outputDir, "assets", "resources");
|
|
220
|
+
await import_fs.default.promises.mkdir(resourcesDir, { recursive: true });
|
|
221
|
+
const snapshotsByFrame = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const snapshot of context.snapshots) {
|
|
223
|
+
let frameSnapshots = snapshotsByFrame.get(snapshot.frameId);
|
|
224
|
+
if (!frameSnapshots) {
|
|
225
|
+
frameSnapshots = [];
|
|
226
|
+
snapshotsByFrame.set(snapshot.frameId, frameSnapshots);
|
|
227
|
+
}
|
|
228
|
+
frameSnapshots.push(snapshot);
|
|
212
229
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
const neededSha1s = /* @__PURE__ */ new Set();
|
|
231
|
+
for (const snapshot of context.snapshots) {
|
|
232
|
+
const frameSnapshots = snapshotsByFrame.get(snapshot.frameId) || [];
|
|
233
|
+
const snapshotIndex = frameSnapshots.indexOf(snapshot);
|
|
234
|
+
for (const override of snapshot.resourceOverrides) {
|
|
235
|
+
if (override.sha1) {
|
|
236
|
+
neededSha1s.add(override.sha1);
|
|
237
|
+
} else if (override.ref !== void 0 && snapshotIndex >= 0) {
|
|
238
|
+
const refIndex = snapshotIndex - override.ref;
|
|
239
|
+
if (refIndex >= 0 && refIndex < frameSnapshots.length) {
|
|
240
|
+
const refSnapshot = frameSnapshots[refIndex];
|
|
241
|
+
const refOverride = refSnapshot.resourceOverrides.find((o) => o.url === override.url);
|
|
242
|
+
if (refOverride?.sha1)
|
|
243
|
+
neededSha1s.add(refOverride.sha1);
|
|
244
|
+
}
|
|
224
245
|
}
|
|
225
246
|
}
|
|
226
247
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
for (const page of context.pages) {
|
|
249
|
+
for (const frame of page.screencastFrames)
|
|
250
|
+
neededSha1s.add(frame.sha1);
|
|
251
|
+
}
|
|
252
|
+
for (const sha1 of context.networkResourceMap.values())
|
|
253
|
+
neededSha1s.add(sha1);
|
|
254
|
+
for (const sha1 of neededSha1s) {
|
|
255
|
+
const resourcePath = `resources/${sha1}`;
|
|
256
|
+
if (!entries.includes(resourcePath))
|
|
257
|
+
continue;
|
|
230
258
|
try {
|
|
231
|
-
const buffer = await zipFile.read(
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const relativePath = `assets/snapshots/${sha1}${ext}`;
|
|
236
|
-
const fullPath = import_path.default.join(outputDir, relativePath);
|
|
237
|
-
await import_fs.default.promises.writeFile(fullPath, text);
|
|
238
|
-
assetMap.set(sha1, `./${relativePath}`);
|
|
239
|
-
}
|
|
259
|
+
const buffer = await zipFile.read(resourcePath);
|
|
260
|
+
const fullPath = import_path.default.join(resourcesDir, sha1);
|
|
261
|
+
await import_fs.default.promises.writeFile(fullPath, buffer);
|
|
262
|
+
assetMap.set(sha1, `./assets/resources/${sha1}`);
|
|
240
263
|
} catch {
|
|
241
264
|
}
|
|
242
265
|
}
|
|
266
|
+
const snapshotsDir = import_path.default.join(outputDir, "assets", "snapshots");
|
|
267
|
+
await import_fs.default.promises.mkdir(snapshotsDir, { recursive: true });
|
|
243
268
|
for (const snapshot of context.snapshots) {
|
|
244
|
-
if (snapshot.html
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
if (!snapshot.html || !snapshot.snapshotName)
|
|
270
|
+
continue;
|
|
271
|
+
try {
|
|
272
|
+
const frameSnapshots = snapshotsByFrame.get(snapshot.frameId) || [];
|
|
273
|
+
const snapshotIndex = frameSnapshots.indexOf(snapshot);
|
|
274
|
+
const renderer = new ExportSnapshotRenderer(frameSnapshots, snapshotIndex, context.networkResourceMap);
|
|
275
|
+
const html = renderer.render();
|
|
276
|
+
for (const sha1 of renderer.getUsedSha1s()) {
|
|
277
|
+
if (!assetMap.has(sha1)) {
|
|
278
|
+
const resourcePath = `resources/${sha1}`;
|
|
279
|
+
if (entries.includes(resourcePath)) {
|
|
280
|
+
try {
|
|
281
|
+
const buffer = await zipFile.read(resourcePath);
|
|
282
|
+
const fullPath2 = import_path.default.join(resourcesDir, sha1);
|
|
283
|
+
await import_fs.default.promises.writeFile(fullPath2, buffer);
|
|
284
|
+
assetMap.set(sha1, `./assets/resources/${sha1}`);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
253
289
|
}
|
|
290
|
+
const safeName = snapshot.snapshotName.replace(/[^a-zA-Z0-9@_-]/g, "_");
|
|
291
|
+
const relativePath = `assets/snapshots/${safeName}.html`;
|
|
292
|
+
const fullPath = import_path.default.join(outputDir, relativePath);
|
|
293
|
+
await import_fs.default.promises.writeFile(fullPath, html);
|
|
294
|
+
assetMap.set(snapshot.snapshotName, `./${relativePath}`);
|
|
295
|
+
} catch {
|
|
254
296
|
}
|
|
255
297
|
}
|
|
256
298
|
zipFile.close();
|
|
257
299
|
return assetMap;
|
|
258
300
|
}
|
|
301
|
+
function generateReadmeMarkdown() {
|
|
302
|
+
return `# Playwright Trace Export
|
|
303
|
+
|
|
304
|
+
This folder contains a Playwright trace exported to LLM-friendly markdown format.
|
|
305
|
+
|
|
306
|
+
## Contents
|
|
307
|
+
|
|
308
|
+
- **index.md** - Overview with test status and error summary
|
|
309
|
+
- **timeline.md** - Step-by-step action timeline with links to DOM snapshots
|
|
310
|
+
- **metadata.md** - Browser and environment information
|
|
311
|
+
- **errors.md** - Full error details with stack traces
|
|
312
|
+
- **console.md** - Browser console output
|
|
313
|
+
- **network.md** - HTTP request log
|
|
314
|
+
|
|
315
|
+
## Viewing DOM Snapshots
|
|
316
|
+
|
|
317
|
+
The exported DOM snapshots include CSS and can be viewed in a browser. Since snapshots use relative paths, you need to serve them via HTTP:
|
|
318
|
+
|
|
319
|
+
\`\`\`bash
|
|
320
|
+
# Using npx serve
|
|
321
|
+
npx serve
|
|
322
|
+
|
|
323
|
+
# Or using Python
|
|
324
|
+
python -m http.server 8000
|
|
325
|
+
\`\`\`
|
|
326
|
+
|
|
327
|
+
Then open the snapshot URLs from \`timeline.md\`, for example:
|
|
328
|
+
- http://localhost:3000/assets/snapshots/after@call@123.html (npx serve)
|
|
329
|
+
- http://localhost:8000/assets/snapshots/after@call@123.html (Python)
|
|
330
|
+
|
|
331
|
+
## Loading Snapshots with Playwright
|
|
332
|
+
|
|
333
|
+
You can load exported snapshots into Playwright for automated DOM inspection:
|
|
334
|
+
|
|
335
|
+
\`\`\`js
|
|
336
|
+
const { chromium } = require('playwright');
|
|
337
|
+
|
|
338
|
+
(async () => {
|
|
339
|
+
const browser = await chromium.launch();
|
|
340
|
+
const page = await browser.newPage();
|
|
341
|
+
|
|
342
|
+
// Serve the export directory first, then:
|
|
343
|
+
await page.goto('http://localhost:3000/assets/snapshots/after@call@123.html');
|
|
344
|
+
|
|
345
|
+
// Inspect the DOM
|
|
346
|
+
const title = await page.title();
|
|
347
|
+
const buttons = await page.locator('button').all();
|
|
348
|
+
console.log(\`Page has \${buttons.length} buttons\`);
|
|
349
|
+
|
|
350
|
+
await browser.close();
|
|
351
|
+
})();
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
This is useful for LLM-based analysis where the AI can navigate and inspect the captured page state.
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
259
357
|
function generateIndexMarkdown(context, traceFile) {
|
|
260
358
|
const title = context.title || "Trace Export";
|
|
261
|
-
const duration = context.endTime - context.startTime;
|
|
359
|
+
const duration = Math.round(context.endTime - context.startTime);
|
|
262
360
|
const actionCount = context.actions.length;
|
|
263
361
|
const errorCount = context.errors.length + context.actions.filter((a) => a.error).length;
|
|
264
362
|
const hasErrors = errorCount > 0;
|
|
@@ -299,10 +397,10 @@ function generateIndexMarkdown(context, traceFile) {
|
|
|
299
397
|
function collectErrorSummary(context) {
|
|
300
398
|
const errors = [];
|
|
301
399
|
for (const error of context.errors)
|
|
302
|
-
errors.push(truncateString(error.message, 100));
|
|
400
|
+
errors.push(truncateString(stripAnsi(error.message), 100));
|
|
303
401
|
for (const action of context.actions) {
|
|
304
402
|
if (action.error)
|
|
305
|
-
errors.push(truncateString(action.error.message, 100));
|
|
403
|
+
errors.push(truncateString(stripAnsi(action.error.message), 100));
|
|
306
404
|
}
|
|
307
405
|
return errors.slice(0, 10);
|
|
308
406
|
}
|
|
@@ -364,58 +462,120 @@ function generateMetadataMarkdown(context) {
|
|
|
364
462
|
`;
|
|
365
463
|
md += `| Wall Time | ${new Date(context.wallTime).toISOString()} |
|
|
366
464
|
`;
|
|
367
|
-
md += `| Duration | ${context.endTime - context.startTime}ms |
|
|
465
|
+
md += `| Duration | ${Math.round(context.endTime - context.startTime)}ms |
|
|
368
466
|
`;
|
|
369
467
|
return md;
|
|
370
468
|
}
|
|
371
|
-
function
|
|
469
|
+
function buildActionTree(actions) {
|
|
470
|
+
const itemMap = /* @__PURE__ */ new Map();
|
|
471
|
+
for (const action of actions) {
|
|
472
|
+
itemMap.set(action.callId, {
|
|
473
|
+
action,
|
|
474
|
+
children: [],
|
|
475
|
+
parent: void 0
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const rootItem = {
|
|
479
|
+
action: {
|
|
480
|
+
callId: "root",
|
|
481
|
+
class: "Root",
|
|
482
|
+
method: "",
|
|
483
|
+
params: {},
|
|
484
|
+
startTime: actions[0]?.startTime || 0,
|
|
485
|
+
endTime: actions[actions.length - 1]?.endTime || 0,
|
|
486
|
+
log: []
|
|
487
|
+
},
|
|
488
|
+
children: [],
|
|
489
|
+
parent: void 0
|
|
490
|
+
};
|
|
491
|
+
for (const item of itemMap.values()) {
|
|
492
|
+
const parent = item.action.parentId ? itemMap.get(item.action.parentId) || rootItem : rootItem;
|
|
493
|
+
parent.children.push(item);
|
|
494
|
+
item.parent = parent;
|
|
495
|
+
}
|
|
496
|
+
const sortChildren = (item) => {
|
|
497
|
+
item.children.sort((a, b) => a.action.startTime - b.action.startTime);
|
|
498
|
+
for (const child of item.children)
|
|
499
|
+
sortChildren(child);
|
|
500
|
+
};
|
|
501
|
+
sortChildren(rootItem);
|
|
502
|
+
return rootItem;
|
|
503
|
+
}
|
|
504
|
+
function getActionTitle(action) {
|
|
505
|
+
if (action.title)
|
|
506
|
+
return action.title;
|
|
507
|
+
return `${action.class}.${action.method}`;
|
|
508
|
+
}
|
|
509
|
+
function buildStepSnapshotMap(actions) {
|
|
510
|
+
const map = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const action of actions) {
|
|
512
|
+
if (action.stepId && (action.beforeSnapshot || action.afterSnapshot)) {
|
|
513
|
+
const existing = map.get(action.stepId) || {};
|
|
514
|
+
if (action.beforeSnapshot)
|
|
515
|
+
existing.before = action.beforeSnapshot;
|
|
516
|
+
if (action.afterSnapshot)
|
|
517
|
+
existing.after = action.afterSnapshot;
|
|
518
|
+
map.set(action.stepId, existing);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return map;
|
|
522
|
+
}
|
|
523
|
+
function generateTimelineMarkdown(actions, assetMap, stepSnapshotMap) {
|
|
372
524
|
if (actions.length === 0)
|
|
373
525
|
return `# Actions Timeline
|
|
374
526
|
|
|
375
527
|
No actions recorded.
|
|
376
528
|
`;
|
|
377
|
-
const
|
|
378
|
-
const
|
|
529
|
+
const filteredActions = actions.filter((action) => action.class === "Test");
|
|
530
|
+
const startTime = filteredActions[0]?.startTime || 0;
|
|
531
|
+
const totalDuration = filteredActions.length > 0 ? (filteredActions[filteredActions.length - 1].endTime || filteredActions[filteredActions.length - 1].startTime) - startTime : 0;
|
|
379
532
|
let md = `# Actions Timeline
|
|
380
533
|
|
|
381
534
|
`;
|
|
382
|
-
md += `Total actions: ${
|
|
383
|
-
|
|
384
|
-
`;
|
|
385
|
-
md += `---
|
|
535
|
+
md += `Total actions: ${filteredActions.length} | Duration: ${Math.round(totalDuration)}ms
|
|
386
536
|
|
|
387
537
|
`;
|
|
388
|
-
|
|
389
|
-
|
|
538
|
+
const rootItem = buildActionTree(filteredActions);
|
|
539
|
+
const renderItem = (item, prefix, index, depth) => {
|
|
540
|
+
const action = item.action;
|
|
541
|
+
if (action.callId === "root")
|
|
542
|
+
return;
|
|
543
|
+
const number = prefix ? `${prefix}.${index}` : `${index}`;
|
|
390
544
|
const relativeTime = action.startTime - startTime;
|
|
391
545
|
const duration = (action.endTime || action.startTime) - action.startTime;
|
|
392
546
|
const hasError = !!action.error;
|
|
393
|
-
|
|
547
|
+
const title = getActionTitle(action);
|
|
548
|
+
const headingLevel = Math.min(depth + 1, 6);
|
|
549
|
+
const heading = "#".repeat(headingLevel);
|
|
550
|
+
md += `${heading} ${number}. ${title}${hasError ? " - ERROR" : ""}
|
|
394
551
|
|
|
395
552
|
`;
|
|
396
|
-
|
|
553
|
+
md += `- **Start:** ${Math.round(relativeTime)}ms
|
|
554
|
+
`;
|
|
555
|
+
md += `- **Duration:** ${Math.round(duration)}ms
|
|
556
|
+
`;
|
|
557
|
+
if (action.params && Object.keys(action.params).length > 0 && action.group !== "internal") {
|
|
397
558
|
const paramsStr = formatParams(action.params);
|
|
398
|
-
|
|
559
|
+
if (paramsStr !== "{}")
|
|
560
|
+
md += `- **Params:** \`${paramsStr}\`
|
|
399
561
|
`;
|
|
400
562
|
}
|
|
401
563
|
if (hasError)
|
|
402
|
-
md += `- **Error:** ${action.error.message}
|
|
564
|
+
md += `- **Error:** ${stripAnsi(action.error.message)}
|
|
403
565
|
`;
|
|
404
|
-
else if (action.result !== void 0)
|
|
566
|
+
else if (action.result !== void 0 && action.group !== "internal")
|
|
405
567
|
md += `- **Result:** ${formatResult(action.result)}
|
|
406
|
-
`;
|
|
407
|
-
else
|
|
408
|
-
md += `- **Result:** Success
|
|
409
|
-
`;
|
|
410
|
-
md += `- **Duration:** ${duration}ms
|
|
411
568
|
`;
|
|
412
569
|
if (action.stack && action.stack.length > 0) {
|
|
413
570
|
const frame = action.stack[0];
|
|
414
571
|
md += `- **Source:** \`${frame.file}:${frame.line}\`
|
|
415
572
|
`;
|
|
416
573
|
}
|
|
417
|
-
const
|
|
418
|
-
const
|
|
574
|
+
const stepSnapshots = stepSnapshotMap.get(action.callId);
|
|
575
|
+
const beforeSnapshotName = action.beforeSnapshot || stepSnapshots?.before;
|
|
576
|
+
const afterSnapshotName = action.afterSnapshot || stepSnapshots?.after;
|
|
577
|
+
const beforeSnapshot = beforeSnapshotName ? resolveSnapshotLink(beforeSnapshotName, assetMap) : null;
|
|
578
|
+
const afterSnapshot = afterSnapshotName ? resolveSnapshotLink(afterSnapshotName, assetMap) : null;
|
|
419
579
|
if (beforeSnapshot || afterSnapshot) {
|
|
420
580
|
const links = [];
|
|
421
581
|
if (beforeSnapshot)
|
|
@@ -443,7 +603,7 @@ No actions recorded.
|
|
|
443
603
|
|
|
444
604
|
`;
|
|
445
605
|
md += "```\n";
|
|
446
|
-
md += `Error: ${action.error.message}
|
|
606
|
+
md += `Error: ${stripAnsi(action.error.message)}
|
|
447
607
|
`;
|
|
448
608
|
for (const frame of action.stack)
|
|
449
609
|
md += ` at ${frame.function || "(anonymous)"} (${frame.file}:${frame.line}:${frame.column})
|
|
@@ -453,10 +613,12 @@ No actions recorded.
|
|
|
453
613
|
`;
|
|
454
614
|
}
|
|
455
615
|
md += `
|
|
456
|
-
---
|
|
457
|
-
|
|
458
616
|
`;
|
|
459
|
-
|
|
617
|
+
for (let i = 0; i < item.children.length; i++)
|
|
618
|
+
renderItem(item.children[i], number, i + 1, depth + 1);
|
|
619
|
+
};
|
|
620
|
+
for (let i = 0; i < rootItem.children.length; i++)
|
|
621
|
+
renderItem(rootItem.children[i], "", i + 1, 1);
|
|
460
622
|
return md;
|
|
461
623
|
}
|
|
462
624
|
function resolveSnapshotLink(snapshotName, assetMap) {
|
|
@@ -472,14 +634,14 @@ function generateErrorsMarkdown(errors, actions) {
|
|
|
472
634
|
const allErrors = [];
|
|
473
635
|
for (const error of errors) {
|
|
474
636
|
allErrors.push({
|
|
475
|
-
message: error.message,
|
|
637
|
+
message: stripAnsi(error.message),
|
|
476
638
|
stack: error.stack
|
|
477
639
|
});
|
|
478
640
|
}
|
|
479
641
|
for (const action of actions) {
|
|
480
642
|
if (action.error) {
|
|
481
643
|
allErrors.push({
|
|
482
|
-
message: action.error.message,
|
|
644
|
+
message: stripAnsi(action.error.message),
|
|
483
645
|
stack: action.stack,
|
|
484
646
|
source: action.stack?.[0] ? `${action.stack[0].file}:${action.stack[0].line}` : void 0
|
|
485
647
|
});
|
|
@@ -604,50 +766,180 @@ No network requests recorded.
|
|
|
604
766
|
}
|
|
605
767
|
return md;
|
|
606
768
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
renderNode(snapshot.html, parts);
|
|
614
|
-
return parts.join("");
|
|
769
|
+
const autoClosing = /* @__PURE__ */ new Set(["AREA", "BASE", "BR", "COL", "COMMAND", "EMBED", "HR", "IMG", "INPUT", "KEYGEN", "LINK", "MENUITEM", "META", "PARAM", "SOURCE", "TRACK", "WBR"]);
|
|
770
|
+
function isNodeNameAttributesChildNodesSnapshot(n) {
|
|
771
|
+
return Array.isArray(n) && typeof n[0] === "string";
|
|
772
|
+
}
|
|
773
|
+
function isSubtreeReferenceSnapshot(n) {
|
|
774
|
+
return Array.isArray(n) && Array.isArray(n[0]);
|
|
615
775
|
}
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
776
|
+
function buildNodeIndex(snapshot) {
|
|
777
|
+
const nodes = [];
|
|
778
|
+
const visit = (n) => {
|
|
779
|
+
if (typeof n === "string") {
|
|
780
|
+
nodes.push(n);
|
|
781
|
+
} else if (isNodeNameAttributesChildNodesSnapshot(n)) {
|
|
782
|
+
const [, , ...children] = n;
|
|
783
|
+
for (const child of children)
|
|
784
|
+
visit(child);
|
|
785
|
+
nodes.push(n);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
visit(snapshot.html);
|
|
789
|
+
return nodes;
|
|
790
|
+
}
|
|
791
|
+
class ExportSnapshotRenderer {
|
|
792
|
+
constructor(snapshots, index, networkResourceMap) {
|
|
793
|
+
this._nodeIndexCache = /* @__PURE__ */ new Map();
|
|
794
|
+
// URL -> SHA1 from network log
|
|
795
|
+
this._usedSha1s = /* @__PURE__ */ new Set();
|
|
796
|
+
this._snapshots = snapshots;
|
|
797
|
+
this._index = index;
|
|
798
|
+
this._snapshot = snapshots[index];
|
|
799
|
+
this._baseUrl = snapshots[index].frameUrl;
|
|
800
|
+
this._networkResourceMap = networkResourceMap;
|
|
801
|
+
this._overrideMap = this._buildOverrideMap();
|
|
802
|
+
}
|
|
803
|
+
// Build a map of URL -> SHA1 from all resourceOverrides, resolving refs
|
|
804
|
+
_buildOverrideMap() {
|
|
805
|
+
const map = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const override of this._snapshot.resourceOverrides) {
|
|
807
|
+
if (override.sha1) {
|
|
808
|
+
map.set(override.url, override.sha1);
|
|
809
|
+
} else if (override.ref !== void 0) {
|
|
810
|
+
const refIndex = this._index - override.ref;
|
|
811
|
+
if (refIndex >= 0 && refIndex < this._snapshots.length) {
|
|
812
|
+
const refSnapshot = this._snapshots[refIndex];
|
|
813
|
+
const refOverride = refSnapshot.resourceOverrides.find((o) => o.url === override.url);
|
|
814
|
+
if (refOverride?.sha1)
|
|
815
|
+
map.set(override.url, refOverride.sha1);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return map;
|
|
620
820
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return;
|
|
628
|
-
let attrs = {};
|
|
629
|
-
let children = [];
|
|
630
|
-
if (rest.length > 0 && typeof rest[0] === "object" && !Array.isArray(rest[0])) {
|
|
631
|
-
attrs = rest[0] || {};
|
|
632
|
-
children = rest.slice(1);
|
|
633
|
-
} else {
|
|
634
|
-
children = rest;
|
|
821
|
+
_getNodeIndex(snapshotIndex) {
|
|
822
|
+
let nodes = this._nodeIndexCache.get(snapshotIndex);
|
|
823
|
+
if (!nodes) {
|
|
824
|
+
nodes = buildNodeIndex(this._snapshots[snapshotIndex]);
|
|
825
|
+
this._nodeIndexCache.set(snapshotIndex, nodes);
|
|
826
|
+
}
|
|
827
|
+
return nodes;
|
|
635
828
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
829
|
+
// Resolve a potentially relative URL to absolute using base URL
|
|
830
|
+
_resolveUrl(url) {
|
|
831
|
+
if (!url || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:"))
|
|
832
|
+
return url;
|
|
833
|
+
try {
|
|
834
|
+
return new URL(url, this._baseUrl).href;
|
|
835
|
+
} catch {
|
|
836
|
+
return url;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Rewrite URL to relative path for export
|
|
840
|
+
_rewriteUrl(url) {
|
|
841
|
+
let sha1 = this._overrideMap.get(url);
|
|
842
|
+
if (!sha1) {
|
|
843
|
+
const resolvedUrl = this._resolveUrl(url);
|
|
844
|
+
sha1 = this._overrideMap.get(resolvedUrl);
|
|
845
|
+
if (!sha1) {
|
|
846
|
+
sha1 = this._networkResourceMap.get(url) || this._networkResourceMap.get(resolvedUrl);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (sha1) {
|
|
850
|
+
this._usedSha1s.add(sha1);
|
|
851
|
+
return `../resources/${sha1}`;
|
|
852
|
+
}
|
|
853
|
+
return url;
|
|
854
|
+
}
|
|
855
|
+
// Rewrite URLs in CSS text (url(...) references)
|
|
856
|
+
_rewriteCssUrls(cssText) {
|
|
857
|
+
return cssText.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (match, quote, url) => {
|
|
858
|
+
const rewritten = this._rewriteUrl(url.trim());
|
|
859
|
+
return `url('${rewritten}')`;
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
// Get all SHA1s that were actually used during rendering
|
|
863
|
+
getUsedSha1s() {
|
|
864
|
+
return this._usedSha1s;
|
|
865
|
+
}
|
|
866
|
+
render() {
|
|
867
|
+
const result = [];
|
|
868
|
+
const visit = (n, snapshotIndex, parentTag) => {
|
|
869
|
+
if (typeof n === "string") {
|
|
870
|
+
if (parentTag === "STYLE" || parentTag === "style")
|
|
871
|
+
result.push(this._rewriteCssUrls((0, import_stringUtils.escapeHTML)(n)));
|
|
872
|
+
else
|
|
873
|
+
result.push((0, import_stringUtils.escapeHTML)(n));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (isSubtreeReferenceSnapshot(n)) {
|
|
877
|
+
const [snapshotsAgo, nodeIndex] = n[0];
|
|
878
|
+
const referenceIndex = snapshotIndex - snapshotsAgo;
|
|
879
|
+
if (referenceIndex >= 0 && referenceIndex <= snapshotIndex) {
|
|
880
|
+
const nodes = this._getNodeIndex(referenceIndex);
|
|
881
|
+
if (nodeIndex >= 0 && nodeIndex < nodes.length)
|
|
882
|
+
return visit(nodes[nodeIndex], referenceIndex, parentTag);
|
|
883
|
+
}
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (isNodeNameAttributesChildNodesSnapshot(n)) {
|
|
887
|
+
const [name, nodeAttrs, ...children] = n;
|
|
888
|
+
const nodeName = name === "NOSCRIPT" ? "X-NOSCRIPT" : name;
|
|
889
|
+
const attrs = Object.entries(nodeAttrs || {});
|
|
890
|
+
if (nodeName === "BASE")
|
|
891
|
+
return;
|
|
892
|
+
result.push("<", nodeName.toLowerCase());
|
|
893
|
+
const isFrame = nodeName === "IFRAME" || nodeName === "FRAME";
|
|
894
|
+
const isAnchor = nodeName === "A";
|
|
895
|
+
const isLink = nodeName === "LINK";
|
|
896
|
+
const isScript = nodeName === "SCRIPT";
|
|
897
|
+
const isImg = nodeName === "IMG";
|
|
898
|
+
for (const [attr, value] of attrs) {
|
|
899
|
+
if (attr.startsWith("__playwright") && attr !== "__playwright_src__")
|
|
900
|
+
continue;
|
|
901
|
+
let attrName = attr;
|
|
902
|
+
let attrValue = value;
|
|
903
|
+
const attrLower = attr.toLowerCase();
|
|
904
|
+
if (isFrame && attr === "__playwright_src__") {
|
|
905
|
+
attrName = "src";
|
|
906
|
+
attrValue = this._rewriteUrl(value);
|
|
907
|
+
} else if (isLink && attrLower === "href") {
|
|
908
|
+
attrValue = this._rewriteUrl(value);
|
|
909
|
+
} else if ((isScript || isImg) && attrLower === "src") {
|
|
910
|
+
attrValue = this._rewriteUrl(value);
|
|
911
|
+
} else if (!isAnchor && !isLink && attrLower === "src") {
|
|
912
|
+
attrValue = this._rewriteUrl(value);
|
|
913
|
+
} else if (attrLower === "srcset") {
|
|
914
|
+
attrValue = this._rewriteSrcset(value);
|
|
915
|
+
} else if (attrLower === "style") {
|
|
916
|
+
attrValue = this._rewriteCssUrls(value);
|
|
917
|
+
}
|
|
918
|
+
result.push(" ", attrName, '="', (0, import_stringUtils.escapeHTMLAttribute)(attrValue), '"');
|
|
919
|
+
}
|
|
920
|
+
result.push(">");
|
|
921
|
+
for (const child of children)
|
|
922
|
+
visit(child, snapshotIndex, nodeName);
|
|
923
|
+
if (!autoClosing.has(nodeName))
|
|
924
|
+
result.push("</", nodeName.toLowerCase(), ">");
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const snapshot = this._snapshot;
|
|
928
|
+
visit(snapshot.html, this._index, void 0);
|
|
929
|
+
const doctype = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : "<!DOCTYPE html>";
|
|
930
|
+
const comment = `<!-- Playwright Snapshot: ${snapshot.snapshotName} | URL: ${snapshot.frameUrl} | Timestamp: ${snapshot.timestamp} -->`;
|
|
931
|
+
return doctype + "\n" + comment + "\n" + result.join("");
|
|
932
|
+
}
|
|
933
|
+
// Rewrite srcset attribute (format: "url1 1x, url2 2x, ...")
|
|
934
|
+
_rewriteSrcset(srcset) {
|
|
935
|
+
return srcset.split(",").map((entry) => {
|
|
936
|
+
const parts = entry.trim().split(/\s+/);
|
|
937
|
+
if (parts.length >= 1) {
|
|
938
|
+
parts[0] = this._rewriteUrl(parts[0]);
|
|
939
|
+
}
|
|
940
|
+
return parts.join(" ");
|
|
941
|
+
}).join(", ");
|
|
641
942
|
}
|
|
642
|
-
parts.push(">");
|
|
643
|
-
for (const child of children)
|
|
644
|
-
renderNode(child, parts);
|
|
645
|
-
const selfClosing = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
|
|
646
|
-
if (!selfClosing.includes(tagName.toLowerCase()))
|
|
647
|
-
parts.push(`</${tagName.toLowerCase()}>`);
|
|
648
|
-
}
|
|
649
|
-
function escapeHtml(text) {
|
|
650
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
651
943
|
}
|
|
652
944
|
function truncateString(str, maxLength) {
|
|
653
945
|
if (str.length <= maxLength)
|
|
@@ -673,6 +965,9 @@ function formatSize(bytes) {
|
|
|
673
965
|
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
674
966
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
675
967
|
}
|
|
968
|
+
function stripAnsi(str) {
|
|
969
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
970
|
+
}
|
|
676
971
|
// Annotate the CommonJS export names for ESM import in node:
|
|
677
972
|
0 && (module.exports = {
|
|
678
973
|
exportTraceToMarkdown
|