@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.
@@ -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
- let assetMap = /* @__PURE__ */ new Map();
45
- if (options.includeAssets) {
46
- await import_fs.default.promises.mkdir(screenshotsDir, { recursive: true });
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: event.snapshot.request?.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 screenshotSha1s = /* @__PURE__ */ new Set();
209
- for (const page of context.pages) {
210
- for (const frame of page.screencastFrames)
211
- screenshotSha1s.add(frame.sha1);
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
- for (const sha1 of screenshotSha1s) {
214
- const resourcePath = `resources/${sha1}`;
215
- if (entries.includes(resourcePath)) {
216
- try {
217
- const buffer = await zipFile.read(resourcePath);
218
- const ext = sha1.includes(".") ? "" : ".png";
219
- const relativePath = `assets/screenshots/${sha1}${ext}`;
220
- const fullPath = import_path.default.join(outputDir, relativePath);
221
- await import_fs.default.promises.writeFile(fullPath, buffer);
222
- assetMap.set(sha1, `./${relativePath}`);
223
- } catch {
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 resourceEntries = entries.filter((name) => name.startsWith("resources/") && !screenshotSha1s.has(name.replace("resources/", "")));
228
- for (const entryName of resourceEntries) {
229
- const sha1 = entryName.replace("resources/", "");
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(entryName);
232
- const text = buffer.toString("utf-8");
233
- if (text.startsWith("<!") || text.startsWith("<html") || text.includes("<head") || text.includes("<body")) {
234
- const ext = sha1.endsWith(".html") ? "" : ".html";
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 && snapshot.snapshotName) {
245
- try {
246
- const html = renderSnapshotToHtml(snapshot);
247
- const safeName = snapshot.snapshotName.replace(/[^a-zA-Z0-9@_-]/g, "_");
248
- const relativePath = `assets/snapshots/${safeName}.html`;
249
- const fullPath = import_path.default.join(outputDir, relativePath);
250
- await import_fs.default.promises.writeFile(fullPath, html);
251
- assetMap.set(snapshot.snapshotName, `./${relativePath}`);
252
- } catch {
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 generateTimelineMarkdown(actions, assetMap) {
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 startTime = actions[0]?.startTime || 0;
378
- const totalDuration = actions.length > 0 ? (actions[actions.length - 1].endTime || actions[actions.length - 1].startTime) - startTime : 0;
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: ${actions.length} | Duration: ${totalDuration}ms
383
-
384
- `;
385
- md += `---
535
+ md += `Total actions: ${filteredActions.length} | Duration: ${Math.round(totalDuration)}ms
386
536
 
387
537
  `;
388
- for (let i = 0; i < actions.length; i++) {
389
- const action = actions[i];
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
- md += `## ${i + 1}. [${relativeTime}ms] ${action.class}.${action.method}${hasError ? " - 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" : ""}
394
551
 
395
552
  `;
396
- if (action.params && Object.keys(action.params).length > 0) {
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
- md += `- **Params:** \`${paramsStr}\`
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 beforeSnapshot = action.beforeSnapshot ? resolveSnapshotLink(action.beforeSnapshot, assetMap) : null;
418
- const afterSnapshot = action.afterSnapshot ? resolveSnapshotLink(action.afterSnapshot, assetMap) : null;
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
- function renderSnapshotToHtml(snapshot) {
608
- const parts = [];
609
- parts.push("<!DOCTYPE html>");
610
- parts.push(`<!-- Playwright Snapshot: ${snapshot.snapshotName} -->`);
611
- parts.push(`<!-- URL: ${snapshot.frameUrl} -->`);
612
- parts.push(`<!-- Timestamp: ${snapshot.timestamp} -->`);
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 renderNode(node, parts) {
617
- if (typeof node === "string") {
618
- parts.push(escapeHtml(node));
619
- return;
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
- if (!Array.isArray(node))
622
- return;
623
- if (Array.isArray(node[0]))
624
- return;
625
- const [tagName, ...rest] = node;
626
- if (typeof tagName !== "string")
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
- parts.push(`<${tagName.toLowerCase()}`);
637
- for (const [key, value] of Object.entries(attrs)) {
638
- if (key.startsWith("__playwright"))
639
- continue;
640
- parts.push(` ${key}="${escapeHtml(String(value))}"`);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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