@pedropaulovc/playwright-core 1.59.0-next.1 → 1.59.0-next.3

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.
@@ -39,7 +39,7 @@ var import_driver = require("./driver");
39
39
  var import_server = require("../server");
40
40
  var import_utils = require("../utils");
41
41
  var import_traceViewer = require("../server/trace/viewer/traceViewer");
42
- var import_traceExporter = require("../server/trace/viewer/traceExporter");
42
+ var import_src = require("../../../../trace-exporter/src");
43
43
  var import_utils2 = require("../utils");
44
44
  var import_ascii = require("../server/utils/ascii");
45
45
  var import_utilsBundle = require("../utilsBundle");
@@ -297,7 +297,7 @@ Examples:
297
297
  $ show-trace https://example.com/trace.zip`);
298
298
  import_utilsBundle.program.command("export-trace <trace>").description("export trace to LLM-friendly markdown files").option("-o, --output <dir>", "output directory", "./trace-export").action(async function(trace, options) {
299
299
  const traceFile = import_path.default.resolve(trace);
300
- await (0, import_traceExporter.exportTraceToMarkdown)(traceFile, {
300
+ await (0, import_src.exportTraceToMarkdown)(traceFile, {
301
301
  outputDir: import_path.default.resolve(options.output)
302
302
  }).catch(logErrorAndExit);
303
303
  console.log(`Trace exported to ${options.output}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedropaulovc/playwright-core",
3
- "version": "1.59.0-next.1",
3
+ "version": "1.59.0-next.3",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,974 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
- var traceExporter_exports = {};
30
- __export(traceExporter_exports, {
31
- exportTraceToMarkdown: () => exportTraceToMarkdown
32
- });
33
- module.exports = __toCommonJS(traceExporter_exports);
34
- var import_fs = __toESM(require("fs"));
35
- var import_path = __toESM(require("path"));
36
- var import_zipFile = require("../../utils/zipFile");
37
- var import_stringUtils = require("../../../utils/isomorphic/stringUtils");
38
- async function exportTraceToMarkdown(traceFile, options) {
39
- const context = await parseTrace(traceFile);
40
- const outputDir = options.outputDir;
41
- const assetsDir = import_path.default.join(outputDir, "assets");
42
- const screenshotsDir = import_path.default.join(assetsDir, "screenshots");
43
- const snapshotsDir = import_path.default.join(assetsDir, "snapshots");
44
- await import_fs.default.promises.mkdir(outputDir, { recursive: true });
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);
48
- const files = [
49
- { name: "README.md", content: generateReadmeMarkdown() },
50
- { name: "index.md", content: generateIndexMarkdown(context, traceFile) },
51
- { name: "metadata.md", content: generateMetadataMarkdown(context) },
52
- { name: "timeline.md", content: generateTimelineMarkdown(context.actions, assetMap, buildStepSnapshotMap(context.actions)) },
53
- { name: "errors.md", content: generateErrorsMarkdown(context.errors, context.actions) },
54
- { name: "console.md", content: generateConsoleMarkdown(context.events) },
55
- { name: "network.md", content: generateNetworkMarkdown(context.resources) }
56
- ];
57
- for (const file of files)
58
- await import_fs.default.promises.writeFile(import_path.default.join(outputDir, file.name), file.content);
59
- }
60
- async function parseTrace(traceFile) {
61
- const zipFile = new import_zipFile.ZipFile(traceFile);
62
- const entries = await zipFile.entries();
63
- const context = {
64
- browserName: "Unknown",
65
- wallTime: 0,
66
- startTime: Number.MAX_SAFE_INTEGER,
67
- endTime: 0,
68
- options: {},
69
- actions: [],
70
- events: [],
71
- errors: [],
72
- resources: [],
73
- pages: [],
74
- snapshots: [],
75
- networkResourceMap: /* @__PURE__ */ new Map()
76
- };
77
- const actionMap = /* @__PURE__ */ new Map();
78
- const pageMap = /* @__PURE__ */ new Map();
79
- const traceEntries = entries.filter((name) => name.endsWith(".trace"));
80
- const networkEntries = entries.filter((name) => name.endsWith(".network"));
81
- for (const entryName of [...traceEntries, ...networkEntries]) {
82
- const content = await zipFile.read(entryName);
83
- const lines = content.toString("utf-8").split("\n");
84
- for (const line of lines) {
85
- if (!line.trim())
86
- continue;
87
- try {
88
- const event = JSON.parse(line);
89
- processTraceEvent(event, context, actionMap, pageMap);
90
- } catch {
91
- }
92
- }
93
- }
94
- context.actions = [...actionMap.values()].sort((a, b) => a.startTime - b.startTime);
95
- context.pages = [...pageMap.values()];
96
- for (const action of context.actions) {
97
- if (action.endTime > context.endTime)
98
- context.endTime = action.endTime;
99
- }
100
- zipFile.close();
101
- return context;
102
- }
103
- function processTraceEvent(event, context, actionMap, pageMap) {
104
- switch (event.type) {
105
- case "context-options":
106
- context.browserName = event.browserName || "Unknown";
107
- context.channel = event.channel;
108
- context.title = event.title;
109
- context.platform = event.platform;
110
- context.playwrightVersion = event.playwrightVersion;
111
- context.wallTime = event.wallTime || 0;
112
- context.startTime = event.monotonicTime || 0;
113
- context.sdkLanguage = event.sdkLanguage;
114
- context.options = event.options || {};
115
- break;
116
- case "before":
117
- actionMap.set(event.callId, {
118
- callId: event.callId,
119
- class: event.class,
120
- method: event.method,
121
- params: event.params || {},
122
- startTime: event.startTime || 0,
123
- endTime: 0,
124
- log: [],
125
- stack: event.stack,
126
- beforeSnapshot: event.beforeSnapshot,
127
- pageId: event.pageId,
128
- parentId: event.parentId,
129
- title: event.title,
130
- group: event.group,
131
- stepId: event.stepId
132
- });
133
- break;
134
- case "after":
135
- const action = actionMap.get(event.callId);
136
- if (action) {
137
- action.endTime = event.endTime || action.startTime;
138
- action.error = event.error;
139
- action.result = event.result;
140
- action.afterSnapshot = event.afterSnapshot;
141
- }
142
- break;
143
- case "log":
144
- const logAction = actionMap.get(event.callId);
145
- if (logAction) {
146
- logAction.log.push({
147
- time: event.time || 0,
148
- message: event.message || ""
149
- });
150
- }
151
- break;
152
- case "console":
153
- context.events.push({
154
- type: "console",
155
- time: event.time || 0,
156
- messageType: event.messageType,
157
- text: event.text,
158
- location: event.location
159
- });
160
- break;
161
- case "error":
162
- context.errors.push({
163
- message: event.message || "Unknown error",
164
- stack: event.stack
165
- });
166
- break;
167
- case "resource-snapshot":
168
- if (event.snapshot) {
169
- const url = event.snapshot.request?.url || "";
170
- const sha1 = event.snapshot.response?.content?._sha1;
171
- context.resources.push({
172
- request: {
173
- method: event.snapshot.request?.method || "GET",
174
- url
175
- },
176
- response: {
177
- status: event.snapshot.response?.status || 0,
178
- content: event.snapshot.response?.content,
179
- _failureText: event.snapshot.response?._failureText
180
- }
181
- });
182
- if (url && sha1)
183
- context.networkResourceMap.set(url, sha1);
184
- }
185
- break;
186
- case "screencast-frame":
187
- let page = pageMap.get(event.pageId);
188
- if (!page) {
189
- page = { pageId: event.pageId, screencastFrames: [] };
190
- pageMap.set(event.pageId, page);
191
- }
192
- page.screencastFrames.push({
193
- sha1: event.sha1,
194
- timestamp: event.timestamp || 0
195
- });
196
- break;
197
- case "frame-snapshot":
198
- if (event.snapshot) {
199
- context.snapshots.push({
200
- snapshotName: event.snapshot.snapshotName || "",
201
- callId: event.snapshot.callId || "",
202
- pageId: event.snapshot.pageId || "",
203
- frameId: event.snapshot.frameId || "",
204
- frameUrl: event.snapshot.frameUrl || "",
205
- html: event.snapshot.html,
206
- timestamp: event.snapshot.timestamp || 0,
207
- resourceOverrides: event.snapshot.resourceOverrides || [],
208
- doctype: event.snapshot.doctype,
209
- viewport: event.snapshot.viewport
210
- });
211
- }
212
- break;
213
- }
214
- }
215
- async function extractAssets(traceFile, context, outputDir) {
216
- const assetMap = /* @__PURE__ */ new Map();
217
- const zipFile = new import_zipFile.ZipFile(traceFile);
218
- const entries = await zipFile.entries();
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);
229
- }
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
- }
245
- }
246
- }
247
- }
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;
258
- try {
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}`);
263
- } catch {
264
- }
265
- }
266
- const snapshotsDir = import_path.default.join(outputDir, "assets", "snapshots");
267
- await import_fs.default.promises.mkdir(snapshotsDir, { recursive: true });
268
- for (const snapshot of context.snapshots) {
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
- }
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 {
296
- }
297
- }
298
- zipFile.close();
299
- return assetMap;
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
- }
357
- function generateIndexMarkdown(context, traceFile) {
358
- const title = context.title || "Trace Export";
359
- const duration = Math.round(context.endTime - context.startTime);
360
- const actionCount = context.actions.length;
361
- const errorCount = context.errors.length + context.actions.filter((a) => a.error).length;
362
- const hasErrors = errorCount > 0;
363
- const errorSummary = collectErrorSummary(context);
364
- let md = `# Trace Export: ${title}
365
-
366
- `;
367
- md += `**Test:** \`${title}\`
368
- `;
369
- md += `**Source:** \`${traceFile}\`
370
-
371
- `;
372
- md += `**Status:** ${hasErrors ? "FAILED" : "PASSED"} | **Duration:** ${duration}ms | **Actions:** ${actionCount} | **Errors:** ${errorCount}
373
-
374
- `;
375
- if (errorSummary.length > 0) {
376
- md += `## Error Summary
377
- `;
378
- for (const error of errorSummary)
379
- md += `- ${error}
380
- `;
381
- md += "\n";
382
- }
383
- md += `## Sections
384
- `;
385
- md += `- [Timeline](./timeline.md) - Step-by-step action timeline
386
- `;
387
- md += `- [Metadata](./metadata.md) - Browser/environment info
388
- `;
389
- md += `- [Errors](./errors.md) - Full error details with stack traces
390
- `;
391
- md += `- [Console](./console.md) - Browser console output
392
- `;
393
- md += `- [Network](./network.md) - HTTP request log
394
- `;
395
- return md;
396
- }
397
- function collectErrorSummary(context) {
398
- const errors = [];
399
- for (const error of context.errors)
400
- errors.push(truncateString(stripAnsi(error.message), 100));
401
- for (const action of context.actions) {
402
- if (action.error)
403
- errors.push(truncateString(stripAnsi(action.error.message), 100));
404
- }
405
- return errors.slice(0, 10);
406
- }
407
- function generateMetadataMarkdown(context) {
408
- let md = `# Trace Metadata
409
-
410
- `;
411
- md += `## Environment
412
-
413
- `;
414
- md += `| Property | Value |
415
- `;
416
- md += `|----------|-------|
417
- `;
418
- md += `| Browser | ${context.browserName || "Unknown"} |
419
- `;
420
- if (context.channel)
421
- md += `| Channel | ${context.channel} |
422
- `;
423
- if (context.platform)
424
- md += `| Platform | ${context.platform} |
425
- `;
426
- if (context.playwrightVersion)
427
- md += `| Playwright Version | ${context.playwrightVersion} |
428
- `;
429
- if (context.sdkLanguage)
430
- md += `| SDK Language | ${context.sdkLanguage} |
431
- `;
432
- md += `
433
- ## Context Options
434
-
435
- `;
436
- md += `| Property | Value |
437
- `;
438
- md += `|----------|-------|
439
- `;
440
- if (context.options.viewport)
441
- md += `| Viewport | ${context.options.viewport.width}x${context.options.viewport.height} |
442
- `;
443
- if (context.options.deviceScaleFactor)
444
- md += `| Device Scale Factor | ${context.options.deviceScaleFactor} |
445
- `;
446
- if (context.options.isMobile !== void 0)
447
- md += `| Mobile | ${context.options.isMobile} |
448
- `;
449
- if (context.options.userAgent)
450
- md += `| User Agent | ${truncateString(context.options.userAgent, 80)} |
451
- `;
452
- if (context.options.baseURL)
453
- md += `| Base URL | ${context.options.baseURL} |
454
- `;
455
- md += `
456
- ## Timing
457
-
458
- `;
459
- md += `| Property | Value |
460
- `;
461
- md += `|----------|-------|
462
- `;
463
- md += `| Wall Time | ${new Date(context.wallTime).toISOString()} |
464
- `;
465
- md += `| Duration | ${Math.round(context.endTime - context.startTime)}ms |
466
- `;
467
- return md;
468
- }
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) {
524
- if (actions.length === 0)
525
- return `# Actions Timeline
526
-
527
- No actions recorded.
528
- `;
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;
532
- let md = `# Actions Timeline
533
-
534
- `;
535
- md += `Total actions: ${filteredActions.length} | Duration: ${Math.round(totalDuration)}ms
536
-
537
- `;
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}`;
544
- const relativeTime = action.startTime - startTime;
545
- const duration = (action.endTime || action.startTime) - action.startTime;
546
- const hasError = !!action.error;
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" : ""}
551
-
552
- `;
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") {
558
- const paramsStr = formatParams(action.params);
559
- if (paramsStr !== "{}")
560
- md += `- **Params:** \`${paramsStr}\`
561
- `;
562
- }
563
- if (hasError)
564
- md += `- **Error:** ${stripAnsi(action.error.message)}
565
- `;
566
- else if (action.result !== void 0 && action.group !== "internal")
567
- md += `- **Result:** ${formatResult(action.result)}
568
- `;
569
- if (action.stack && action.stack.length > 0) {
570
- const frame = action.stack[0];
571
- md += `- **Source:** \`${frame.file}:${frame.line}\`
572
- `;
573
- }
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;
579
- if (beforeSnapshot || afterSnapshot) {
580
- const links = [];
581
- if (beforeSnapshot)
582
- links.push(`[before](${beforeSnapshot})`);
583
- if (afterSnapshot)
584
- links.push(`[after](${afterSnapshot})`);
585
- md += `- **Snapshots:** ${links.join(" | ")}
586
- `;
587
- }
588
- if (action.log && action.log.length > 0) {
589
- md += `
590
- <details><summary>Action Log</summary>
591
-
592
- `;
593
- for (const entry of action.log)
594
- md += `- ${entry.message}
595
- `;
596
- md += `
597
- </details>
598
- `;
599
- }
600
- if (hasError && action.stack && action.stack.length > 0) {
601
- md += `
602
- <details><summary>Stack Trace</summary>
603
-
604
- `;
605
- md += "```\n";
606
- md += `Error: ${stripAnsi(action.error.message)}
607
- `;
608
- for (const frame of action.stack)
609
- md += ` at ${frame.function || "(anonymous)"} (${frame.file}:${frame.line}:${frame.column})
610
- `;
611
- md += "```\n\n";
612
- md += `</details>
613
- `;
614
- }
615
- md += `
616
- `;
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);
622
- return md;
623
- }
624
- function resolveSnapshotLink(snapshotName, assetMap) {
625
- if (assetMap.has(snapshotName))
626
- return assetMap.get(snapshotName);
627
- for (const [key, assetPath] of assetMap) {
628
- if (snapshotName.includes(key) || key.includes(snapshotName))
629
- return assetPath;
630
- }
631
- return null;
632
- }
633
- function generateErrorsMarkdown(errors, actions) {
634
- const allErrors = [];
635
- for (const error of errors) {
636
- allErrors.push({
637
- message: stripAnsi(error.message),
638
- stack: error.stack
639
- });
640
- }
641
- for (const action of actions) {
642
- if (action.error) {
643
- allErrors.push({
644
- message: stripAnsi(action.error.message),
645
- stack: action.stack,
646
- source: action.stack?.[0] ? `${action.stack[0].file}:${action.stack[0].line}` : void 0
647
- });
648
- }
649
- }
650
- if (allErrors.length === 0)
651
- return `# Errors
652
-
653
- No errors recorded.
654
- `;
655
- let md = `# Errors
656
-
657
- `;
658
- md += `Total errors: ${allErrors.length}
659
-
660
- `;
661
- for (let i = 0; i < allErrors.length; i++) {
662
- const error = allErrors[i];
663
- md += `## Error ${i + 1}
664
-
665
- `;
666
- md += `**Message:** ${error.message}
667
-
668
- `;
669
- if (error.source)
670
- md += `**Source:** \`${error.source}\`
671
-
672
- `;
673
- if (error.stack && error.stack.length > 0) {
674
- md += `**Stack Trace:**
675
-
676
- `;
677
- md += "```\n";
678
- for (const frame of error.stack)
679
- md += ` at ${frame.function || "(anonymous)"} (${frame.file}:${frame.line}:${frame.column})
680
- `;
681
- md += "```\n\n";
682
- }
683
- md += `---
684
-
685
- `;
686
- }
687
- return md;
688
- }
689
- function generateConsoleMarkdown(events) {
690
- const consoleEvents = events.filter((e) => e.type === "console");
691
- if (consoleEvents.length === 0)
692
- return `# Console Log
693
-
694
- No console messages recorded.
695
- `;
696
- let md = `# Console Log
697
-
698
- `;
699
- md += `Total messages: ${consoleEvents.length}
700
-
701
- `;
702
- md += `| Time | Type | Message | Location |
703
- `;
704
- md += `|------|------|---------|----------|
705
- `;
706
- for (const event of consoleEvents) {
707
- const message = truncateString(event.text || "", 100).replace(/\|/g, "\\|").replace(/\n/g, " ");
708
- const location = event.location ? `${event.location.url}:${event.location.lineNumber}` : "";
709
- md += `| ${event.time}ms | ${event.messageType || "log"} | ${message} | ${truncateString(location, 50)} |
710
- `;
711
- }
712
- return md;
713
- }
714
- function generateNetworkMarkdown(resources) {
715
- if (resources.length === 0)
716
- return `# Network Log
717
-
718
- No network requests recorded.
719
- `;
720
- let md = `# Network Log
721
-
722
- `;
723
- md += `Total requests: ${resources.length}
724
-
725
- `;
726
- md += `| # | Method | URL | Status | Size |
727
- `;
728
- md += `|---|--------|-----|--------|------|
729
- `;
730
- const failedRequests = [];
731
- for (let i = 0; i < resources.length; i++) {
732
- const resource = resources[i];
733
- const url = truncateString(resource.request.url, 60);
734
- const status = resource.response.status;
735
- const size = formatSize(resource.response.content?.size || 0);
736
- md += `| ${i + 1} | ${resource.request.method} | ${url} | ${status} | ${size} |
737
- `;
738
- if (status >= 400)
739
- failedRequests.push(resource);
740
- }
741
- if (failedRequests.length > 0) {
742
- md += `
743
- ## Failed Requests
744
-
745
- `;
746
- for (const resource of failedRequests) {
747
- md += `### ${resource.request.method} ${truncateString(resource.request.url, 80)} - ${resource.response.status}
748
-
749
- `;
750
- if (resource.response._failureText)
751
- md += `**Failure:** ${resource.response._failureText}
752
-
753
- `;
754
- if (resource.response.content?.text) {
755
- md += `<details><summary>Response</summary>
756
-
757
- `;
758
- md += "```\n";
759
- md += truncateString(resource.response.content.text, 1e3);
760
- md += "\n```\n\n";
761
- md += `</details>
762
-
763
- `;
764
- }
765
- }
766
- }
767
- return md;
768
- }
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]);
775
- }
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;
820
- }
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;
828
- }
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(", ");
942
- }
943
- }
944
- function truncateString(str, maxLength) {
945
- if (str.length <= maxLength)
946
- return str;
947
- return str.substring(0, maxLength - 3) + "...";
948
- }
949
- function formatParams(params) {
950
- const str = JSON.stringify(params);
951
- return truncateString(str, 200);
952
- }
953
- function formatResult(result) {
954
- if (result === null || result === void 0)
955
- return "null";
956
- if (typeof result === "string")
957
- return truncateString(result, 100);
958
- const str = JSON.stringify(result);
959
- return truncateString(str, 100);
960
- }
961
- function formatSize(bytes) {
962
- if (bytes < 1024)
963
- return `${bytes}B`;
964
- if (bytes < 1024 * 1024)
965
- return `${(bytes / 1024).toFixed(1)}KB`;
966
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
967
- }
968
- function stripAnsi(str) {
969
- return str.replace(/\x1b\[[0-9;]*m/g, "");
970
- }
971
- // Annotate the CommonJS export names for ESM import in node:
972
- 0 && (module.exports = {
973
- exportTraceToMarkdown
974
- });