executable-stories-formatters 0.7.0 → 0.7.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/dist/index.cjs CHANGED
@@ -47,6 +47,7 @@ __export(src_exports, {
47
47
  adaptPlaywrightRun: () => adaptPlaywrightRun,
48
48
  adaptVitestRun: () => adaptVitestRun,
49
49
  assertValidRun: () => assertValidRun,
50
+ bundleAssets: () => bundleAssets,
50
51
  calculateFlakiness: () => calculateFlakiness,
51
52
  calculateStability: () => calculateStability,
52
53
  canonicalizeRun: () => canonicalizeRun,
@@ -98,8 +99,8 @@ __export(src_exports, {
98
99
  validateCanonicalRun: () => validateCanonicalRun
99
100
  });
100
101
  module.exports = __toCommonJS(src_exports);
101
- var fs4 = require("fs");
102
- var path4 = __toESM(require("path"), 1);
102
+ var fs6 = require("fs");
103
+ var path6 = __toESM(require("path"), 1);
103
104
  var fsPromises = __toESM(require("fs/promises"), 1);
104
105
 
105
106
  // src/converters/acl/status.ts
@@ -351,6 +352,9 @@ function canonicalizeTestCase(raw, options, projectRoot) {
351
352
  projectRoot
352
353
  });
353
354
  const tags = normalizeTags(story);
355
+ if (story.tickets) {
356
+ story.tickets = normalizeTickets(story.tickets);
357
+ }
354
358
  const titlePath = buildTitlePath(raw, story);
355
359
  return {
356
360
  id,
@@ -374,6 +378,9 @@ function normalizeTags(story) {
374
378
  const tags = story.tags ?? [];
375
379
  return [...new Set(tags)].sort();
376
380
  }
381
+ function normalizeTickets(raw) {
382
+ return raw.map((t) => typeof t === "string" ? { id: t } : t);
383
+ }
377
384
  function buildTitlePath(raw, story) {
378
385
  if (story.suitePath && story.suitePath.length > 0) {
379
386
  return story.suitePath;
@@ -2692,6 +2699,16 @@ body {
2692
2699
  background: none;
2693
2700
  }
2694
2701
 
2702
+ /* ============================================================================
2703
+ Documentation Entries - Children
2704
+ ============================================================================ */
2705
+ .doc-children {
2706
+ margin-left: 1rem;
2707
+ padding-left: 1rem;
2708
+ border-left: 2px solid var(--border);
2709
+ margin-top: 0.25rem;
2710
+ }
2711
+
2695
2712
  /* ============================================================================
2696
2713
  Trace View - OTel span waterfall
2697
2714
  ============================================================================ */
@@ -12406,30 +12423,46 @@ function renderDocCustom(entry, deps) {
12406
12423
  </div>`;
12407
12424
  }
12408
12425
  function renderDocEntry(entry, deps) {
12426
+ let html;
12409
12427
  switch (entry.kind) {
12410
12428
  case "note":
12411
- return renderDocNote(entry, deps);
12429
+ html = renderDocNote(entry, deps);
12430
+ break;
12412
12431
  case "tag":
12413
- return renderDocTag(entry, deps);
12432
+ html = renderDocTag(entry, deps);
12433
+ break;
12414
12434
  case "kv":
12415
- return renderDocKv(entry, deps);
12435
+ html = renderDocKv(entry, deps);
12436
+ break;
12416
12437
  case "code":
12417
- return renderDocCode(entry, deps);
12438
+ html = renderDocCode(entry, deps);
12439
+ break;
12418
12440
  case "table":
12419
- return renderDocTable(entry, deps);
12441
+ html = renderDocTable(entry, deps);
12442
+ break;
12420
12443
  case "link":
12421
- return renderDocLink(entry, deps);
12444
+ html = renderDocLink(entry, deps);
12445
+ break;
12422
12446
  case "section":
12423
- return renderDocSection(entry, deps);
12447
+ html = renderDocSection(entry, deps);
12448
+ break;
12424
12449
  case "mermaid":
12425
- return renderDocMermaid(entry, deps);
12450
+ html = renderDocMermaid(entry, deps);
12451
+ break;
12426
12452
  case "screenshot":
12427
- return renderDocScreenshot(entry, deps);
12453
+ html = renderDocScreenshot(entry, deps);
12454
+ break;
12428
12455
  case "custom":
12429
- return renderDocCustom(entry, deps);
12456
+ html = renderDocCustom(entry, deps);
12457
+ break;
12430
12458
  default:
12431
- return "";
12459
+ html = "";
12460
+ }
12461
+ if (entry.children && entry.children.length > 0) {
12462
+ const childrenHtml = entry.children.map((child) => renderDocEntry(child, deps)).join("");
12463
+ html += `<div class="doc-children">${childrenHtml}</div>`;
12432
12464
  }
12465
+ return html;
12433
12466
  }
12434
12467
 
12435
12468
  // src/formatters/html/renderers/steps.ts
@@ -12491,12 +12524,20 @@ function hasSufficientHistory(entries, min) {
12491
12524
  }
12492
12525
 
12493
12526
  // src/formatters/html/renderers/scenario.ts
12527
+ function renderTicket(ticket, template, escapeHtml3) {
12528
+ const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
12529
+ if (url) {
12530
+ return `<a class="tag ticket-tag" href="${escapeHtml3(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml3(ticket.id)}</a>`;
12531
+ }
12532
+ return `<span class="tag ticket-tag">${escapeHtml3(ticket.id)}</span>`;
12533
+ }
12494
12534
  function renderScenario(args, deps) {
12495
12535
  const { tc } = args;
12496
12536
  const statusIcon = deps.getStatusIcon(tc.status);
12497
12537
  const statusClass = `status-${tc.status}`;
12498
12538
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
12499
12539
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
12540
+ const tickets = (tc.story.tickets ?? []).map((t) => renderTicket(t, deps.ticketUrlTemplate, deps.escapeHtml)).join("");
12500
12541
  const otelMeta = tc.story.meta?.otel;
12501
12542
  let traceBadge = "";
12502
12543
  if (otelMeta?.traceId) {
@@ -12564,7 +12605,7 @@ function renderScenario(args, deps) {
12564
12605
  <span class="status-icon ${statusClass}">${statusIcon}</span>
12565
12606
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
12566
12607
  </div>
12567
- <div class="scenario-meta">${tags}${sourceLink}${traceBadge}${metricBadges}</div>
12608
+ <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
12568
12609
  </div>
12569
12610
  <span class="scenario-duration">${duration}</span>
12570
12611
  </div>
@@ -12909,6 +12950,7 @@ function normalizeOptions(options = {}) {
12909
12950
  mermaidEnabled: options.mermaidEnabled ?? true,
12910
12951
  markdownEnabled: options.markdownEnabled ?? true,
12911
12952
  permalinkBaseUrl: options.permalinkBaseUrl,
12953
+ ticketUrlTemplate: options.ticketUrlTemplate,
12912
12954
  theme: options.theme ?? "default"
12913
12955
  };
12914
12956
  }
@@ -12941,7 +12983,8 @@ function createHtmlFormatter(options = {}) {
12941
12983
  renderAttachments: (args, d) => renderAttachments(args, d),
12942
12984
  renderTraceView: (args, d) => renderTraceView(args, d),
12943
12985
  embedScreenshots: opts.embedScreenshots,
12944
- permalinkBaseUrl: opts.permalinkBaseUrl
12986
+ permalinkBaseUrl: opts.permalinkBaseUrl,
12987
+ ticketUrlTemplate: opts.ticketUrlTemplate
12945
12988
  };
12946
12989
  const featureDeps = {
12947
12990
  escapeHtml,
@@ -13454,14 +13497,16 @@ var MarkdownFormatter = class {
13454
13497
  }
13455
13498
  if (tc.story.tickets && tc.story.tickets.length > 0) {
13456
13499
  const ticketTemplate = this.options.ticketUrlTemplate;
13457
- if (ticketTemplate) {
13458
- const ticketLinks = tc.story.tickets.map(
13459
- (t) => `[${t}](${ticketTemplate.replace("{ticket}", t)})`
13460
- );
13461
- meta.push(`Tickets: ${ticketLinks.join(", ")}`);
13462
- } else {
13463
- meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
13464
- }
13500
+ const ticketLinks = tc.story.tickets.map((t) => {
13501
+ if (t.url) {
13502
+ return `[${t.id}](${t.url})`;
13503
+ }
13504
+ if (ticketTemplate) {
13505
+ return `[${t.id}](${ticketTemplate.replace("{ticket}", t.id)})`;
13506
+ }
13507
+ return `\`${t.id}\``;
13508
+ });
13509
+ meta.push(`Tickets: ${ticketLinks.join(", ")}`);
13465
13510
  }
13466
13511
  const otelMeta = tc.story.meta?.otel;
13467
13512
  if (otelMeta?.traceId) {
@@ -13635,6 +13680,12 @@ var MarkdownFormatter = class {
13635
13680
  lines.push(`${indent}`);
13636
13681
  break;
13637
13682
  }
13683
+ if (entry.children && entry.children.length > 0) {
13684
+ const childIndent = indent + " ";
13685
+ for (const child of entry.children) {
13686
+ this.renderDocEntry(lines, child, childIndent);
13687
+ }
13688
+ }
13638
13689
  }
13639
13690
  /**
13640
13691
  * Get status icon for a status.
@@ -13707,8 +13758,8 @@ function extractFeatureName(testCases, uri) {
13707
13758
  return tc.titlePath[0];
13708
13759
  }
13709
13760
  }
13710
- const basename2 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
13711
- return basename2.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13761
+ const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
13762
+ return basename3.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13712
13763
  }
13713
13764
  function synthesizeFeature(uri, testCases) {
13714
13765
  const featureName = extractFeatureName(testCases, uri);
@@ -14320,8 +14371,8 @@ function extractDocAttachments(step) {
14320
14371
  }
14321
14372
  return attachments;
14322
14373
  }
14323
- function guessMediaType(path5) {
14324
- const lower = path5.toLowerCase();
14374
+ function guessMediaType(path7) {
14375
+ const lower = path7.toLowerCase();
14325
14376
  if (lower.endsWith(".png")) return "image/png";
14326
14377
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
14327
14378
  if (lower.endsWith(".gif")) return "image/gif";
@@ -14462,11 +14513,11 @@ var CucumberHtmlFormatter = class {
14462
14513
  for (const envelope of envelopes) {
14463
14514
  const accepted = htmlStream.write(envelope);
14464
14515
  if (!accepted) {
14465
- await new Promise((resolve4) => htmlStream.once("drain", resolve4));
14516
+ await new Promise((resolve5) => htmlStream.once("drain", resolve5));
14466
14517
  }
14467
14518
  }
14468
- await new Promise((resolve4, reject) => {
14469
- collector.on("finish", resolve4);
14519
+ await new Promise((resolve5, reject) => {
14520
+ collector.on("finish", resolve5);
14470
14521
  collector.on("error", reject);
14471
14522
  htmlStream.end();
14472
14523
  });
@@ -14584,7 +14635,7 @@ function buildFlags(baseline, current) {
14584
14635
  steps: stableJson(baseline.story.steps) !== stableJson(current.story.steps),
14585
14636
  docs: stableJson(baselineDocs) !== stableJson(currentDocs),
14586
14637
  tags: !compareStringArrays(baseline.tags, current.tags),
14587
- tickets: !compareStringArrays(baseline.story.tickets ?? [], current.story.tickets ?? []),
14638
+ tickets: stableJson(baseline.story.tickets ?? []) !== stableJson(current.story.tickets ?? []),
14588
14639
  source: baseline.sourceFile !== current.sourceFile || baseline.sourceLine !== current.sourceLine,
14589
14640
  duration: baseline.durationMs !== current.durationMs,
14590
14641
  attachments: stableJson(baseline.attachments) !== stableJson(current.attachments),
@@ -14807,7 +14858,7 @@ function renderScenarioCard(scenario) {
14807
14858
  <dd>${escapeHtml2(before.errorMessage ?? "") || "&nbsp;"}</dd>
14808
14859
  ${scenario.flags.steps ? `<dt>Steps</dt><dd>${formatSteps(before.steps)}</dd>` : ""}
14809
14860
  ${scenario.flags.docs ? `<dt>Docs</dt><dd>${formatDocs(before.docs)}</dd>` : ""}
14810
- ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(before.tickets.join(", ")) || "&nbsp;"}</dd>` : ""}
14861
+ ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(before.tickets.map((t) => t.id).join(", ")) || "&nbsp;"}</dd>` : ""}
14811
14862
  </dl>
14812
14863
  </section>
14813
14864
  <section>
@@ -14821,7 +14872,7 @@ function renderScenarioCard(scenario) {
14821
14872
  <dd>${escapeHtml2(after.errorMessage ?? "") || "&nbsp;"}</dd>
14822
14873
  ${scenario.flags.steps ? `<dt>Steps</dt><dd>${formatSteps(after.steps)}</dd>` : ""}
14823
14874
  ${scenario.flags.docs ? `<dt>Docs</dt><dd>${formatDocs(after.docs)}</dd>` : ""}
14824
- ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(after.tickets.join(", ")) || "&nbsp;"}</dd>` : ""}
14875
+ ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(after.tickets.map((t) => t.id).join(", ")) || "&nbsp;"}</dd>` : ""}
14825
14876
  </dl>
14826
14877
  </section>
14827
14878
  </div>` : (() => {
@@ -14835,7 +14886,7 @@ function renderScenarioCard(scenario) {
14835
14886
  return `<div class="snapshot-detail">
14836
14887
  <dl>
14837
14888
  ${hasTags ? `<dt>Tags</dt><dd>${escapeHtml2(snapshot.tags.join(", "))}</dd>` : ""}
14838
- ${hasTickets ? `<dt>Tickets</dt><dd>${escapeHtml2(snapshot.tickets.join(", "))}</dd>` : ""}
14889
+ ${hasTickets ? `<dt>Tickets</dt><dd>${escapeHtml2(snapshot.tickets.map((t) => t.id).join(", "))}</dd>` : ""}
14839
14890
  ${hasSteps ? `<dt>Steps</dt><dd>${formatSteps(snapshot.steps)}</dd>` : ""}
14840
14891
  ${hasDocs ? `<dt>Docs</dt><dd>${formatDocs(snapshot.docs)}</dd>` : ""}
14841
14892
  </dl>
@@ -15150,7 +15201,7 @@ function renderScenario2(lines, scenario) {
15150
15201
  lines.push(`| Docs | ${escapeCell(formatDocs2(before.docs))} | ${escapeCell(formatDocs2(after.docs))} |`);
15151
15202
  }
15152
15203
  if (scenario.flags.tickets) {
15153
- lines.push(`| Tickets | ${escapeCell(before.tickets.join(", "))} | ${escapeCell(after.tickets.join(", "))} |`);
15204
+ lines.push(`| Tickets | ${escapeCell(before.tickets.map((t) => t.id).join(", "))} | ${escapeCell(after.tickets.map((t) => t.id).join(", "))} |`);
15154
15205
  }
15155
15206
  lines.push("");
15156
15207
  } else {
@@ -15166,7 +15217,7 @@ function renderSnapshotDetail(lines, snapshot) {
15166
15217
  lines.push("");
15167
15218
  }
15168
15219
  if (snapshot.tickets.length > 0) {
15169
- lines.push(`**Tickets:** ${snapshot.tickets.join(", ")}`);
15220
+ lines.push(`**Tickets:** ${snapshot.tickets.map((t) => t.id).join(", ")}`);
15170
15221
  lines.push("");
15171
15222
  }
15172
15223
  if (snapshot.steps.length > 0) {
@@ -15364,6 +15415,110 @@ function selectTestCases(args, deps) {
15364
15415
  return sortTestCases(selected, sortMode);
15365
15416
  }
15366
15417
 
15418
+ // src/bundler/bundle-assets.ts
15419
+ var fs3 = __toESM(require("fs"), 1);
15420
+ var path3 = __toESM(require("path"), 1);
15421
+
15422
+ // src/bundler/scan-html-assets.ts
15423
+ function scanHtmlAssets(html) {
15424
+ const seen = /* @__PURE__ */ new Set();
15425
+ const patterns = [
15426
+ /<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
15427
+ /<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
15428
+ /<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
15429
+ ];
15430
+ for (const pattern of patterns) {
15431
+ let match;
15432
+ while ((match = pattern.exec(html)) !== null) {
15433
+ const ref = match[1];
15434
+ if (isLocalAssetRef(ref) && !seen.has(ref)) {
15435
+ seen.add(ref);
15436
+ }
15437
+ }
15438
+ }
15439
+ return [...seen];
15440
+ }
15441
+ function isLocalAssetRef(ref) {
15442
+ if (!ref) return false;
15443
+ if (ref.startsWith("data:")) return false;
15444
+ if (ref.startsWith("http://") || ref.startsWith("https://")) return false;
15445
+ if (ref.startsWith("#")) return false;
15446
+ return true;
15447
+ }
15448
+
15449
+ // src/bundler/copy-asset.ts
15450
+ var fs2 = __toESM(require("fs"), 1);
15451
+ var path2 = __toESM(require("path"), 1);
15452
+ var crypto = __toESM(require("crypto"), 1);
15453
+ function copyAsset(sourcePath, assetsDir) {
15454
+ if (!fs2.existsSync(assetsDir)) {
15455
+ fs2.mkdirSync(assetsDir, { recursive: true });
15456
+ }
15457
+ const content = fs2.readFileSync(sourcePath);
15458
+ const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
15459
+ const ext = path2.extname(sourcePath);
15460
+ const baseName = sanitize(path2.basename(sourcePath, ext));
15461
+ const destName = `${baseName}-${hash}${ext}`;
15462
+ const destPath = path2.join(assetsDir, destName);
15463
+ if (!fs2.existsSync(destPath)) {
15464
+ fs2.copyFileSync(sourcePath, destPath);
15465
+ }
15466
+ return `assets/${destName}`;
15467
+ }
15468
+ function sanitize(name) {
15469
+ return name.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
15470
+ }
15471
+
15472
+ // src/bundler/bundle-assets.ts
15473
+ function bundleAssets(htmlPath, options = {}) {
15474
+ const htmlDir = path3.dirname(htmlPath);
15475
+ const assetsDir = path3.join(htmlDir, "assets");
15476
+ let html = fs3.readFileSync(htmlPath, "utf8");
15477
+ const refs = scanHtmlAssets(html);
15478
+ let copiedCount = 0;
15479
+ const missing = [];
15480
+ for (const ref of refs) {
15481
+ const absolutePath = path3.resolve(htmlDir, ref);
15482
+ if (!fs3.existsSync(absolutePath)) {
15483
+ missing.push(ref);
15484
+ continue;
15485
+ }
15486
+ const newRelPath = copyAsset(absolutePath, assetsDir);
15487
+ html = replaceAssetRef(html, ref, newRelPath);
15488
+ copiedCount++;
15489
+ }
15490
+ if (missing.length > 0 && !options.allowMissing) {
15491
+ throw new Error(
15492
+ `Missing asset${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
15493
+ );
15494
+ }
15495
+ fs3.writeFileSync(htmlPath, html, "utf8");
15496
+ return {
15497
+ copiedCount,
15498
+ missingCount: missing.length,
15499
+ missing
15500
+ };
15501
+ }
15502
+ function replaceAssetRef(html, original, replacement) {
15503
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15504
+ const srcPattern = new RegExp(
15505
+ `(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
15506
+ "g"
15507
+ );
15508
+ html = html.replace(srcPattern, `$1${replacement}$2`);
15509
+ const hrefClassFirst = new RegExp(
15510
+ `(<a\\b[^>]*?\\bclass=["']attachment["'][^>]*?\\bhref=["'])${escaped}(["'])`,
15511
+ "g"
15512
+ );
15513
+ html = html.replace(hrefClassFirst, `$1${replacement}$2`);
15514
+ const hrefHrefFirst = new RegExp(
15515
+ `(<a\\b[^>]*?\\bhref=["'])${escaped}(["'][^>]*?\\bclass=["']attachment["'])`,
15516
+ "g"
15517
+ );
15518
+ html = html.replace(hrefHrefFirst, `$1${replacement}$2`);
15519
+ return html;
15520
+ }
15521
+
15367
15522
  // src/converters/adapters/jest.ts
15368
15523
  function mapJestStatus(status) {
15369
15524
  switch (status) {
@@ -16131,27 +16286,27 @@ function pickleStepArgumentToDocs(ps) {
16131
16286
  }
16132
16287
 
16133
16288
  // src/utils/git-info.ts
16134
- var fs2 = __toESM(require("fs"), 1);
16135
- var path2 = __toESM(require("path"), 1);
16289
+ var fs4 = __toESM(require("fs"), 1);
16290
+ var path4 = __toESM(require("path"), 1);
16136
16291
  function readGitSha(cwd = process.cwd()) {
16137
16292
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
16138
16293
  if (envSha) return envSha;
16139
16294
  const gitDir = findGitDir(cwd);
16140
16295
  if (!gitDir) return void 0;
16141
16296
  try {
16142
- const headPath = path2.join(gitDir, "HEAD");
16143
- const head = fs2.readFileSync(headPath, "utf8").trim();
16297
+ const headPath = path4.join(gitDir, "HEAD");
16298
+ const head = fs4.readFileSync(headPath, "utf8").trim();
16144
16299
  if (!head.startsWith("ref:")) {
16145
16300
  return head;
16146
16301
  }
16147
16302
  const refPath = head.replace("ref:", "").trim();
16148
- const refFile = path2.join(gitDir, refPath);
16149
- if (fs2.existsSync(refFile)) {
16150
- return fs2.readFileSync(refFile, "utf8").trim();
16303
+ const refFile = path4.join(gitDir, refPath);
16304
+ if (fs4.existsSync(refFile)) {
16305
+ return fs4.readFileSync(refFile, "utf8").trim();
16151
16306
  }
16152
- const packedRefs = path2.join(gitDir, "packed-refs");
16153
- if (fs2.existsSync(packedRefs)) {
16154
- const content = fs2.readFileSync(packedRefs, "utf8");
16307
+ const packedRefs = path4.join(gitDir, "packed-refs");
16308
+ if (fs4.existsSync(packedRefs)) {
16309
+ const content = fs4.readFileSync(packedRefs, "utf8");
16155
16310
  for (const line of content.split("\n")) {
16156
16311
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
16157
16312
  const [sha, ref] = line.split(" ");
@@ -16166,19 +16321,19 @@ function readGitSha(cwd = process.cwd()) {
16166
16321
  function findGitDir(start) {
16167
16322
  let current = start;
16168
16323
  while (true) {
16169
- const candidate = path2.join(current, ".git");
16170
- if (fs2.existsSync(candidate)) {
16171
- const stat = fs2.statSync(candidate);
16324
+ const candidate = path4.join(current, ".git");
16325
+ if (fs4.existsSync(candidate)) {
16326
+ const stat = fs4.statSync(candidate);
16172
16327
  if (stat.isFile()) {
16173
- const content = fs2.readFileSync(candidate, "utf8").trim();
16328
+ const content = fs4.readFileSync(candidate, "utf8").trim();
16174
16329
  const match = content.match(/^gitdir: (.+)$/);
16175
16330
  if (match) {
16176
- return path2.resolve(current, match[1]);
16331
+ return path4.resolve(current, match[1]);
16177
16332
  }
16178
16333
  }
16179
16334
  return candidate;
16180
16335
  }
16181
- const parent = path2.dirname(current);
16336
+ const parent = path4.dirname(current);
16182
16337
  if (parent === current) return void 0;
16183
16338
  current = parent;
16184
16339
  }
@@ -16189,8 +16344,8 @@ function readBranchName(cwd = process.cwd()) {
16189
16344
  const gitDir = findGitDir(cwd);
16190
16345
  if (!gitDir) return void 0;
16191
16346
  try {
16192
- const headPath = path2.join(gitDir, "HEAD");
16193
- const head = fs2.readFileSync(headPath, "utf8").trim();
16347
+ const headPath = path4.join(gitDir, "HEAD");
16348
+ const head = fs4.readFileSync(headPath, "utf8").trim();
16194
16349
  if (head.startsWith("ref:")) {
16195
16350
  const refPath = head.replace("ref:", "").trim();
16196
16351
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -16227,8 +16382,8 @@ function nanosecondsToMs(ns) {
16227
16382
  }
16228
16383
 
16229
16384
  // src/utils/metadata.ts
16230
- var fs3 = __toESM(require("fs"), 1);
16231
- var path3 = __toESM(require("path"), 1);
16385
+ var fs5 = __toESM(require("fs"), 1);
16386
+ var path5 = __toESM(require("path"), 1);
16232
16387
  var versionCache = /* @__PURE__ */ new Map();
16233
16388
  function readPackageVersion(root) {
16234
16389
  if (versionCache.has(root)) {
@@ -16239,18 +16394,18 @@ function readPackageVersion(root) {
16239
16394
  return version;
16240
16395
  }
16241
16396
  function findPackageVersion(startDir) {
16242
- let current = path3.resolve(startDir);
16397
+ let current = path5.resolve(startDir);
16243
16398
  while (true) {
16244
- const pkgPath = path3.join(current, "package.json");
16399
+ const pkgPath = path5.join(current, "package.json");
16245
16400
  try {
16246
- if (fs3.existsSync(pkgPath)) {
16247
- const raw = fs3.readFileSync(pkgPath, "utf8");
16401
+ if (fs5.existsSync(pkgPath)) {
16402
+ const raw = fs5.readFileSync(pkgPath, "utf8");
16248
16403
  const parsed = JSON.parse(raw);
16249
16404
  return parsed.version;
16250
16405
  }
16251
16406
  } catch {
16252
16407
  }
16253
- const parent = path3.dirname(current);
16408
+ const parent = path5.dirname(current);
16254
16409
  if (parent === current) {
16255
16410
  return void 0;
16256
16411
  }
@@ -17208,11 +17363,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17208
17363
  const ext = FORMAT_EXTENSIONS[format];
17209
17364
  const effectiveName = outputName + (outputNameSuffix ?? "");
17210
17365
  if (mode === "aggregated") {
17211
- return toPosix(path4.join(baseOutputDir, `${effectiveName}${ext}`));
17366
+ return toPosix(path6.join(baseOutputDir, `${effectiveName}${ext}`));
17212
17367
  }
17213
17368
  const normalizedSource = toPosix(sourceFile);
17214
- const dirOfSource = path4.posix.dirname(normalizedSource);
17215
- let baseName = path4.posix.basename(normalizedSource);
17369
+ const dirOfSource = path6.posix.dirname(normalizedSource);
17370
+ let baseName = path6.posix.basename(normalizedSource);
17216
17371
  for (const testExt of TEST_EXTENSIONS) {
17217
17372
  if (baseName.endsWith(testExt)) {
17218
17373
  baseName = baseName.slice(0, -testExt.length);
@@ -17221,9 +17376,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17221
17376
  }
17222
17377
  const fileName = `${baseName}.${effectiveName}${ext}`;
17223
17378
  if (colocatedStyle === "adjacent") {
17224
- return toPosix(path4.posix.join(dirOfSource, fileName));
17379
+ return toPosix(path6.posix.join(dirOfSource, fileName));
17225
17380
  }
17226
- return toPosix(path4.posix.join(baseOutputDir, dirOfSource, fileName));
17381
+ return toPosix(path6.posix.join(baseOutputDir, dirOfSource, fileName));
17227
17382
  }
17228
17383
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
17229
17384
  const groups = /* @__PURE__ */ new Map();
@@ -17322,6 +17477,7 @@ var ReportGenerator = class {
17322
17477
  mermaidEnabled: options.html?.mermaidEnabled ?? true,
17323
17478
  markdownEnabled: options.html?.markdownEnabled ?? true,
17324
17479
  permalinkBaseUrl: options.html?.permalinkBaseUrl,
17480
+ ticketUrlTemplate: options.html?.ticketUrlTemplate,
17325
17481
  theme: options.html?.theme ?? "default"
17326
17482
  },
17327
17483
  junit: {
@@ -17345,7 +17501,9 @@ var ReportGenerator = class {
17345
17501
  traceUrlTemplate: options.markdown?.traceUrlTemplate,
17346
17502
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
17347
17503
  customRenderers: options.markdown?.customRenderers
17348
- }
17504
+ },
17505
+ assetMode: options.assetMode ?? "none",
17506
+ allowMissingAssets: options.allowMissingAssets ?? false
17349
17507
  };
17350
17508
  }
17351
17509
  /**
@@ -17372,6 +17530,16 @@ var ReportGenerator = class {
17372
17530
  const paths = await this.generateFormat(filteredRun, format);
17373
17531
  results.set(format, paths);
17374
17532
  }
17533
+ if (this.options.assetMode === "copy") {
17534
+ const htmlPaths = results.get("html");
17535
+ if (htmlPaths) {
17536
+ for (const htmlPath of htmlPaths) {
17537
+ bundleAssets(htmlPath, {
17538
+ allowMissing: this.options.allowMissingAssets
17539
+ });
17540
+ }
17541
+ }
17542
+ }
17375
17543
  return results;
17376
17544
  }
17377
17545
  /**
@@ -17389,9 +17557,9 @@ var ReportGenerator = class {
17389
17557
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
17390
17558
  const ext = FORMAT_EXTENSIONS[format];
17391
17559
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
17392
- const outputPath = toPosix(path4.join(this.options.outputDir, `${effectiveName}${ext}`));
17560
+ const outputPath = toPosix(path6.join(this.options.outputDir, `${effectiveName}${ext}`));
17393
17561
  const content = await this.formatContent(run, format);
17394
- const dir = path4.dirname(outputPath);
17562
+ const dir = path6.dirname(outputPath);
17395
17563
  await fsPromises.mkdir(dir, { recursive: true });
17396
17564
  await this.deps.writeFile(outputPath, content);
17397
17565
  return [outputPath];
@@ -17403,7 +17571,7 @@ var ReportGenerator = class {
17403
17571
  testCases
17404
17572
  };
17405
17573
  const content = await this.formatContent(groupRun, format);
17406
- const dir = path4.dirname(outputPath);
17574
+ const dir = path6.dirname(outputPath);
17407
17575
  await fsPromises.mkdir(dir, { recursive: true });
17408
17576
  await this.deps.writeFile(outputPath, content);
17409
17577
  writtenPaths.push(outputPath);
@@ -17432,7 +17600,8 @@ var ReportGenerator = class {
17432
17600
  syntaxHighlighting: this.options.html.syntaxHighlighting,
17433
17601
  mermaidEnabled: this.options.html.mermaidEnabled,
17434
17602
  markdownEnabled: this.options.html.markdownEnabled,
17435
- permalinkBaseUrl: this.options.html.permalinkBaseUrl
17603
+ permalinkBaseUrl: this.options.html.permalinkBaseUrl,
17604
+ ticketUrlTemplate: this.options.html.ticketUrlTemplate
17436
17605
  });
17437
17606
  return formatter.format(run);
17438
17607
  }
@@ -17500,7 +17669,7 @@ async function generateRunComparison(args) {
17500
17669
  await fsPromises.mkdir(outputDir, { recursive: true });
17501
17670
  for (const format of args.formats) {
17502
17671
  const ext = format === "html" ? ".html" : ".md";
17503
- const outputPath = toPosix(path4.join(outputDir, `${outputName}${ext}`));
17672
+ const outputPath = toPosix(path6.join(outputDir, `${outputName}${ext}`));
17504
17673
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
17505
17674
  await fsPromises.writeFile(outputPath, content, "utf8");
17506
17675
  files.push(outputPath);
@@ -17538,6 +17707,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
17538
17707
  adaptPlaywrightRun,
17539
17708
  adaptVitestRun,
17540
17709
  assertValidRun,
17710
+ bundleAssets,
17541
17711
  calculateFlakiness,
17542
17712
  calculateStability,
17543
17713
  canonicalizeRun,