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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path4 from "path";
3
+ import * as path6 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -252,6 +252,9 @@ function canonicalizeTestCase(raw, options, projectRoot) {
252
252
  projectRoot
253
253
  });
254
254
  const tags = normalizeTags(story);
255
+ if (story.tickets) {
256
+ story.tickets = normalizeTickets(story.tickets);
257
+ }
255
258
  const titlePath = buildTitlePath(raw, story);
256
259
  return {
257
260
  id,
@@ -275,6 +278,9 @@ function normalizeTags(story) {
275
278
  const tags = story.tags ?? [];
276
279
  return [...new Set(tags)].sort();
277
280
  }
281
+ function normalizeTickets(raw) {
282
+ return raw.map((t) => typeof t === "string" ? { id: t } : t);
283
+ }
278
284
  function buildTitlePath(raw, story) {
279
285
  if (story.suitePath && story.suitePath.length > 0) {
280
286
  return story.suitePath;
@@ -2593,6 +2599,16 @@ body {
2593
2599
  background: none;
2594
2600
  }
2595
2601
 
2602
+ /* ============================================================================
2603
+ Documentation Entries - Children
2604
+ ============================================================================ */
2605
+ .doc-children {
2606
+ margin-left: 1rem;
2607
+ padding-left: 1rem;
2608
+ border-left: 2px solid var(--border);
2609
+ margin-top: 0.25rem;
2610
+ }
2611
+
2596
2612
  /* ============================================================================
2597
2613
  Trace View - OTel span waterfall
2598
2614
  ============================================================================ */
@@ -12307,30 +12323,46 @@ function renderDocCustom(entry, deps) {
12307
12323
  </div>`;
12308
12324
  }
12309
12325
  function renderDocEntry(entry, deps) {
12326
+ let html;
12310
12327
  switch (entry.kind) {
12311
12328
  case "note":
12312
- return renderDocNote(entry, deps);
12329
+ html = renderDocNote(entry, deps);
12330
+ break;
12313
12331
  case "tag":
12314
- return renderDocTag(entry, deps);
12332
+ html = renderDocTag(entry, deps);
12333
+ break;
12315
12334
  case "kv":
12316
- return renderDocKv(entry, deps);
12335
+ html = renderDocKv(entry, deps);
12336
+ break;
12317
12337
  case "code":
12318
- return renderDocCode(entry, deps);
12338
+ html = renderDocCode(entry, deps);
12339
+ break;
12319
12340
  case "table":
12320
- return renderDocTable(entry, deps);
12341
+ html = renderDocTable(entry, deps);
12342
+ break;
12321
12343
  case "link":
12322
- return renderDocLink(entry, deps);
12344
+ html = renderDocLink(entry, deps);
12345
+ break;
12323
12346
  case "section":
12324
- return renderDocSection(entry, deps);
12347
+ html = renderDocSection(entry, deps);
12348
+ break;
12325
12349
  case "mermaid":
12326
- return renderDocMermaid(entry, deps);
12350
+ html = renderDocMermaid(entry, deps);
12351
+ break;
12327
12352
  case "screenshot":
12328
- return renderDocScreenshot(entry, deps);
12353
+ html = renderDocScreenshot(entry, deps);
12354
+ break;
12329
12355
  case "custom":
12330
- return renderDocCustom(entry, deps);
12356
+ html = renderDocCustom(entry, deps);
12357
+ break;
12331
12358
  default:
12332
- return "";
12359
+ html = "";
12360
+ }
12361
+ if (entry.children && entry.children.length > 0) {
12362
+ const childrenHtml = entry.children.map((child) => renderDocEntry(child, deps)).join("");
12363
+ html += `<div class="doc-children">${childrenHtml}</div>`;
12333
12364
  }
12365
+ return html;
12334
12366
  }
12335
12367
 
12336
12368
  // src/formatters/html/renderers/steps.ts
@@ -12392,12 +12424,20 @@ function hasSufficientHistory(entries, min) {
12392
12424
  }
12393
12425
 
12394
12426
  // src/formatters/html/renderers/scenario.ts
12427
+ function renderTicket(ticket, template, escapeHtml3) {
12428
+ const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
12429
+ if (url) {
12430
+ return `<a class="tag ticket-tag" href="${escapeHtml3(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml3(ticket.id)}</a>`;
12431
+ }
12432
+ return `<span class="tag ticket-tag">${escapeHtml3(ticket.id)}</span>`;
12433
+ }
12395
12434
  function renderScenario(args, deps) {
12396
12435
  const { tc } = args;
12397
12436
  const statusIcon = deps.getStatusIcon(tc.status);
12398
12437
  const statusClass = `status-${tc.status}`;
12399
12438
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
12400
12439
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
12440
+ const tickets = (tc.story.tickets ?? []).map((t) => renderTicket(t, deps.ticketUrlTemplate, deps.escapeHtml)).join("");
12401
12441
  const otelMeta = tc.story.meta?.otel;
12402
12442
  let traceBadge = "";
12403
12443
  if (otelMeta?.traceId) {
@@ -12465,7 +12505,7 @@ function renderScenario(args, deps) {
12465
12505
  <span class="status-icon ${statusClass}">${statusIcon}</span>
12466
12506
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
12467
12507
  </div>
12468
- <div class="scenario-meta">${tags}${sourceLink}${traceBadge}${metricBadges}</div>
12508
+ <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
12469
12509
  </div>
12470
12510
  <span class="scenario-duration">${duration}</span>
12471
12511
  </div>
@@ -12810,6 +12850,7 @@ function normalizeOptions(options = {}) {
12810
12850
  mermaidEnabled: options.mermaidEnabled ?? true,
12811
12851
  markdownEnabled: options.markdownEnabled ?? true,
12812
12852
  permalinkBaseUrl: options.permalinkBaseUrl,
12853
+ ticketUrlTemplate: options.ticketUrlTemplate,
12813
12854
  theme: options.theme ?? "default"
12814
12855
  };
12815
12856
  }
@@ -12842,7 +12883,8 @@ function createHtmlFormatter(options = {}) {
12842
12883
  renderAttachments: (args, d) => renderAttachments(args, d),
12843
12884
  renderTraceView: (args, d) => renderTraceView(args, d),
12844
12885
  embedScreenshots: opts.embedScreenshots,
12845
- permalinkBaseUrl: opts.permalinkBaseUrl
12886
+ permalinkBaseUrl: opts.permalinkBaseUrl,
12887
+ ticketUrlTemplate: opts.ticketUrlTemplate
12846
12888
  };
12847
12889
  const featureDeps = {
12848
12890
  escapeHtml,
@@ -13355,14 +13397,16 @@ var MarkdownFormatter = class {
13355
13397
  }
13356
13398
  if (tc.story.tickets && tc.story.tickets.length > 0) {
13357
13399
  const ticketTemplate = this.options.ticketUrlTemplate;
13358
- if (ticketTemplate) {
13359
- const ticketLinks = tc.story.tickets.map(
13360
- (t) => `[${t}](${ticketTemplate.replace("{ticket}", t)})`
13361
- );
13362
- meta.push(`Tickets: ${ticketLinks.join(", ")}`);
13363
- } else {
13364
- meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
13365
- }
13400
+ const ticketLinks = tc.story.tickets.map((t) => {
13401
+ if (t.url) {
13402
+ return `[${t.id}](${t.url})`;
13403
+ }
13404
+ if (ticketTemplate) {
13405
+ return `[${t.id}](${ticketTemplate.replace("{ticket}", t.id)})`;
13406
+ }
13407
+ return `\`${t.id}\``;
13408
+ });
13409
+ meta.push(`Tickets: ${ticketLinks.join(", ")}`);
13366
13410
  }
13367
13411
  const otelMeta = tc.story.meta?.otel;
13368
13412
  if (otelMeta?.traceId) {
@@ -13536,6 +13580,12 @@ var MarkdownFormatter = class {
13536
13580
  lines.push(`${indent}`);
13537
13581
  break;
13538
13582
  }
13583
+ if (entry.children && entry.children.length > 0) {
13584
+ const childIndent = indent + " ";
13585
+ for (const child of entry.children) {
13586
+ this.renderDocEntry(lines, child, childIndent);
13587
+ }
13588
+ }
13539
13589
  }
13540
13590
  /**
13541
13591
  * Get status icon for a status.
@@ -13608,8 +13658,8 @@ function extractFeatureName(testCases, uri) {
13608
13658
  return tc.titlePath[0];
13609
13659
  }
13610
13660
  }
13611
- const basename2 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
13612
- return basename2.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13661
+ const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
13662
+ return basename3.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13613
13663
  }
13614
13664
  function synthesizeFeature(uri, testCases) {
13615
13665
  const featureName = extractFeatureName(testCases, uri);
@@ -14221,8 +14271,8 @@ function extractDocAttachments(step) {
14221
14271
  }
14222
14272
  return attachments;
14223
14273
  }
14224
- function guessMediaType(path5) {
14225
- const lower = path5.toLowerCase();
14274
+ function guessMediaType(path7) {
14275
+ const lower = path7.toLowerCase();
14226
14276
  if (lower.endsWith(".png")) return "image/png";
14227
14277
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
14228
14278
  if (lower.endsWith(".gif")) return "image/gif";
@@ -14363,11 +14413,11 @@ var CucumberHtmlFormatter = class {
14363
14413
  for (const envelope of envelopes) {
14364
14414
  const accepted = htmlStream.write(envelope);
14365
14415
  if (!accepted) {
14366
- await new Promise((resolve4) => htmlStream.once("drain", resolve4));
14416
+ await new Promise((resolve5) => htmlStream.once("drain", resolve5));
14367
14417
  }
14368
14418
  }
14369
- await new Promise((resolve4, reject) => {
14370
- collector.on("finish", resolve4);
14419
+ await new Promise((resolve5, reject) => {
14420
+ collector.on("finish", resolve5);
14371
14421
  collector.on("error", reject);
14372
14422
  htmlStream.end();
14373
14423
  });
@@ -14485,7 +14535,7 @@ function buildFlags(baseline, current) {
14485
14535
  steps: stableJson(baseline.story.steps) !== stableJson(current.story.steps),
14486
14536
  docs: stableJson(baselineDocs) !== stableJson(currentDocs),
14487
14537
  tags: !compareStringArrays(baseline.tags, current.tags),
14488
- tickets: !compareStringArrays(baseline.story.tickets ?? [], current.story.tickets ?? []),
14538
+ tickets: stableJson(baseline.story.tickets ?? []) !== stableJson(current.story.tickets ?? []),
14489
14539
  source: baseline.sourceFile !== current.sourceFile || baseline.sourceLine !== current.sourceLine,
14490
14540
  duration: baseline.durationMs !== current.durationMs,
14491
14541
  attachments: stableJson(baseline.attachments) !== stableJson(current.attachments),
@@ -14708,7 +14758,7 @@ function renderScenarioCard(scenario) {
14708
14758
  <dd>${escapeHtml2(before.errorMessage ?? "") || "&nbsp;"}</dd>
14709
14759
  ${scenario.flags.steps ? `<dt>Steps</dt><dd>${formatSteps(before.steps)}</dd>` : ""}
14710
14760
  ${scenario.flags.docs ? `<dt>Docs</dt><dd>${formatDocs(before.docs)}</dd>` : ""}
14711
- ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(before.tickets.join(", ")) || "&nbsp;"}</dd>` : ""}
14761
+ ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(before.tickets.map((t) => t.id).join(", ")) || "&nbsp;"}</dd>` : ""}
14712
14762
  </dl>
14713
14763
  </section>
14714
14764
  <section>
@@ -14722,7 +14772,7 @@ function renderScenarioCard(scenario) {
14722
14772
  <dd>${escapeHtml2(after.errorMessage ?? "") || "&nbsp;"}</dd>
14723
14773
  ${scenario.flags.steps ? `<dt>Steps</dt><dd>${formatSteps(after.steps)}</dd>` : ""}
14724
14774
  ${scenario.flags.docs ? `<dt>Docs</dt><dd>${formatDocs(after.docs)}</dd>` : ""}
14725
- ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(after.tickets.join(", ")) || "&nbsp;"}</dd>` : ""}
14775
+ ${scenario.flags.tickets ? `<dt>Tickets</dt><dd>${escapeHtml2(after.tickets.map((t) => t.id).join(", ")) || "&nbsp;"}</dd>` : ""}
14726
14776
  </dl>
14727
14777
  </section>
14728
14778
  </div>` : (() => {
@@ -14736,7 +14786,7 @@ function renderScenarioCard(scenario) {
14736
14786
  return `<div class="snapshot-detail">
14737
14787
  <dl>
14738
14788
  ${hasTags ? `<dt>Tags</dt><dd>${escapeHtml2(snapshot.tags.join(", "))}</dd>` : ""}
14739
- ${hasTickets ? `<dt>Tickets</dt><dd>${escapeHtml2(snapshot.tickets.join(", "))}</dd>` : ""}
14789
+ ${hasTickets ? `<dt>Tickets</dt><dd>${escapeHtml2(snapshot.tickets.map((t) => t.id).join(", "))}</dd>` : ""}
14740
14790
  ${hasSteps ? `<dt>Steps</dt><dd>${formatSteps(snapshot.steps)}</dd>` : ""}
14741
14791
  ${hasDocs ? `<dt>Docs</dt><dd>${formatDocs(snapshot.docs)}</dd>` : ""}
14742
14792
  </dl>
@@ -15051,7 +15101,7 @@ function renderScenario2(lines, scenario) {
15051
15101
  lines.push(`| Docs | ${escapeCell(formatDocs2(before.docs))} | ${escapeCell(formatDocs2(after.docs))} |`);
15052
15102
  }
15053
15103
  if (scenario.flags.tickets) {
15054
- lines.push(`| Tickets | ${escapeCell(before.tickets.join(", "))} | ${escapeCell(after.tickets.join(", "))} |`);
15104
+ lines.push(`| Tickets | ${escapeCell(before.tickets.map((t) => t.id).join(", "))} | ${escapeCell(after.tickets.map((t) => t.id).join(", "))} |`);
15055
15105
  }
15056
15106
  lines.push("");
15057
15107
  } else {
@@ -15067,7 +15117,7 @@ function renderSnapshotDetail(lines, snapshot) {
15067
15117
  lines.push("");
15068
15118
  }
15069
15119
  if (snapshot.tickets.length > 0) {
15070
- lines.push(`**Tickets:** ${snapshot.tickets.join(", ")}`);
15120
+ lines.push(`**Tickets:** ${snapshot.tickets.map((t) => t.id).join(", ")}`);
15071
15121
  lines.push("");
15072
15122
  }
15073
15123
  if (snapshot.steps.length > 0) {
@@ -15265,6 +15315,110 @@ function selectTestCases(args, deps) {
15265
15315
  return sortTestCases(selected, sortMode);
15266
15316
  }
15267
15317
 
15318
+ // src/bundler/bundle-assets.ts
15319
+ import * as fs3 from "fs";
15320
+ import * as path3 from "path";
15321
+
15322
+ // src/bundler/scan-html-assets.ts
15323
+ function scanHtmlAssets(html) {
15324
+ const seen = /* @__PURE__ */ new Set();
15325
+ const patterns = [
15326
+ /<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
15327
+ /<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
15328
+ /<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
15329
+ ];
15330
+ for (const pattern of patterns) {
15331
+ let match;
15332
+ while ((match = pattern.exec(html)) !== null) {
15333
+ const ref = match[1];
15334
+ if (isLocalAssetRef(ref) && !seen.has(ref)) {
15335
+ seen.add(ref);
15336
+ }
15337
+ }
15338
+ }
15339
+ return [...seen];
15340
+ }
15341
+ function isLocalAssetRef(ref) {
15342
+ if (!ref) return false;
15343
+ if (ref.startsWith("data:")) return false;
15344
+ if (ref.startsWith("http://") || ref.startsWith("https://")) return false;
15345
+ if (ref.startsWith("#")) return false;
15346
+ return true;
15347
+ }
15348
+
15349
+ // src/bundler/copy-asset.ts
15350
+ import * as fs2 from "fs";
15351
+ import * as path2 from "path";
15352
+ import * as crypto from "crypto";
15353
+ function copyAsset(sourcePath, assetsDir) {
15354
+ if (!fs2.existsSync(assetsDir)) {
15355
+ fs2.mkdirSync(assetsDir, { recursive: true });
15356
+ }
15357
+ const content = fs2.readFileSync(sourcePath);
15358
+ const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
15359
+ const ext = path2.extname(sourcePath);
15360
+ const baseName = sanitize(path2.basename(sourcePath, ext));
15361
+ const destName = `${baseName}-${hash}${ext}`;
15362
+ const destPath = path2.join(assetsDir, destName);
15363
+ if (!fs2.existsSync(destPath)) {
15364
+ fs2.copyFileSync(sourcePath, destPath);
15365
+ }
15366
+ return `assets/${destName}`;
15367
+ }
15368
+ function sanitize(name) {
15369
+ return name.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
15370
+ }
15371
+
15372
+ // src/bundler/bundle-assets.ts
15373
+ function bundleAssets(htmlPath, options = {}) {
15374
+ const htmlDir = path3.dirname(htmlPath);
15375
+ const assetsDir = path3.join(htmlDir, "assets");
15376
+ let html = fs3.readFileSync(htmlPath, "utf8");
15377
+ const refs = scanHtmlAssets(html);
15378
+ let copiedCount = 0;
15379
+ const missing = [];
15380
+ for (const ref of refs) {
15381
+ const absolutePath = path3.resolve(htmlDir, ref);
15382
+ if (!fs3.existsSync(absolutePath)) {
15383
+ missing.push(ref);
15384
+ continue;
15385
+ }
15386
+ const newRelPath = copyAsset(absolutePath, assetsDir);
15387
+ html = replaceAssetRef(html, ref, newRelPath);
15388
+ copiedCount++;
15389
+ }
15390
+ if (missing.length > 0 && !options.allowMissing) {
15391
+ throw new Error(
15392
+ `Missing asset${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
15393
+ );
15394
+ }
15395
+ fs3.writeFileSync(htmlPath, html, "utf8");
15396
+ return {
15397
+ copiedCount,
15398
+ missingCount: missing.length,
15399
+ missing
15400
+ };
15401
+ }
15402
+ function replaceAssetRef(html, original, replacement) {
15403
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15404
+ const srcPattern = new RegExp(
15405
+ `(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
15406
+ "g"
15407
+ );
15408
+ html = html.replace(srcPattern, `$1${replacement}$2`);
15409
+ const hrefClassFirst = new RegExp(
15410
+ `(<a\\b[^>]*?\\bclass=["']attachment["'][^>]*?\\bhref=["'])${escaped}(["'])`,
15411
+ "g"
15412
+ );
15413
+ html = html.replace(hrefClassFirst, `$1${replacement}$2`);
15414
+ const hrefHrefFirst = new RegExp(
15415
+ `(<a\\b[^>]*?\\bhref=["'])${escaped}(["'][^>]*?\\bclass=["']attachment["'])`,
15416
+ "g"
15417
+ );
15418
+ html = html.replace(hrefHrefFirst, `$1${replacement}$2`);
15419
+ return html;
15420
+ }
15421
+
15268
15422
  // src/converters/adapters/jest.ts
15269
15423
  function mapJestStatus(status) {
15270
15424
  switch (status) {
@@ -16032,27 +16186,27 @@ function pickleStepArgumentToDocs(ps) {
16032
16186
  }
16033
16187
 
16034
16188
  // src/utils/git-info.ts
16035
- import * as fs2 from "fs";
16036
- import * as path2 from "path";
16189
+ import * as fs4 from "fs";
16190
+ import * as path4 from "path";
16037
16191
  function readGitSha(cwd = process.cwd()) {
16038
16192
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
16039
16193
  if (envSha) return envSha;
16040
16194
  const gitDir = findGitDir(cwd);
16041
16195
  if (!gitDir) return void 0;
16042
16196
  try {
16043
- const headPath = path2.join(gitDir, "HEAD");
16044
- const head = fs2.readFileSync(headPath, "utf8").trim();
16197
+ const headPath = path4.join(gitDir, "HEAD");
16198
+ const head = fs4.readFileSync(headPath, "utf8").trim();
16045
16199
  if (!head.startsWith("ref:")) {
16046
16200
  return head;
16047
16201
  }
16048
16202
  const refPath = head.replace("ref:", "").trim();
16049
- const refFile = path2.join(gitDir, refPath);
16050
- if (fs2.existsSync(refFile)) {
16051
- return fs2.readFileSync(refFile, "utf8").trim();
16203
+ const refFile = path4.join(gitDir, refPath);
16204
+ if (fs4.existsSync(refFile)) {
16205
+ return fs4.readFileSync(refFile, "utf8").trim();
16052
16206
  }
16053
- const packedRefs = path2.join(gitDir, "packed-refs");
16054
- if (fs2.existsSync(packedRefs)) {
16055
- const content = fs2.readFileSync(packedRefs, "utf8");
16207
+ const packedRefs = path4.join(gitDir, "packed-refs");
16208
+ if (fs4.existsSync(packedRefs)) {
16209
+ const content = fs4.readFileSync(packedRefs, "utf8");
16056
16210
  for (const line of content.split("\n")) {
16057
16211
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
16058
16212
  const [sha, ref] = line.split(" ");
@@ -16067,19 +16221,19 @@ function readGitSha(cwd = process.cwd()) {
16067
16221
  function findGitDir(start) {
16068
16222
  let current = start;
16069
16223
  while (true) {
16070
- const candidate = path2.join(current, ".git");
16071
- if (fs2.existsSync(candidate)) {
16072
- const stat = fs2.statSync(candidate);
16224
+ const candidate = path4.join(current, ".git");
16225
+ if (fs4.existsSync(candidate)) {
16226
+ const stat = fs4.statSync(candidate);
16073
16227
  if (stat.isFile()) {
16074
- const content = fs2.readFileSync(candidate, "utf8").trim();
16228
+ const content = fs4.readFileSync(candidate, "utf8").trim();
16075
16229
  const match = content.match(/^gitdir: (.+)$/);
16076
16230
  if (match) {
16077
- return path2.resolve(current, match[1]);
16231
+ return path4.resolve(current, match[1]);
16078
16232
  }
16079
16233
  }
16080
16234
  return candidate;
16081
16235
  }
16082
- const parent = path2.dirname(current);
16236
+ const parent = path4.dirname(current);
16083
16237
  if (parent === current) return void 0;
16084
16238
  current = parent;
16085
16239
  }
@@ -16090,8 +16244,8 @@ function readBranchName(cwd = process.cwd()) {
16090
16244
  const gitDir = findGitDir(cwd);
16091
16245
  if (!gitDir) return void 0;
16092
16246
  try {
16093
- const headPath = path2.join(gitDir, "HEAD");
16094
- const head = fs2.readFileSync(headPath, "utf8").trim();
16247
+ const headPath = path4.join(gitDir, "HEAD");
16248
+ const head = fs4.readFileSync(headPath, "utf8").trim();
16095
16249
  if (head.startsWith("ref:")) {
16096
16250
  const refPath = head.replace("ref:", "").trim();
16097
16251
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -16128,8 +16282,8 @@ function nanosecondsToMs(ns) {
16128
16282
  }
16129
16283
 
16130
16284
  // src/utils/metadata.ts
16131
- import * as fs3 from "fs";
16132
- import * as path3 from "path";
16285
+ import * as fs5 from "fs";
16286
+ import * as path5 from "path";
16133
16287
  var versionCache = /* @__PURE__ */ new Map();
16134
16288
  function readPackageVersion(root) {
16135
16289
  if (versionCache.has(root)) {
@@ -16140,18 +16294,18 @@ function readPackageVersion(root) {
16140
16294
  return version;
16141
16295
  }
16142
16296
  function findPackageVersion(startDir) {
16143
- let current = path3.resolve(startDir);
16297
+ let current = path5.resolve(startDir);
16144
16298
  while (true) {
16145
- const pkgPath = path3.join(current, "package.json");
16299
+ const pkgPath = path5.join(current, "package.json");
16146
16300
  try {
16147
- if (fs3.existsSync(pkgPath)) {
16148
- const raw = fs3.readFileSync(pkgPath, "utf8");
16301
+ if (fs5.existsSync(pkgPath)) {
16302
+ const raw = fs5.readFileSync(pkgPath, "utf8");
16149
16303
  const parsed = JSON.parse(raw);
16150
16304
  return parsed.version;
16151
16305
  }
16152
16306
  } catch {
16153
16307
  }
16154
- const parent = path3.dirname(current);
16308
+ const parent = path5.dirname(current);
16155
16309
  if (parent === current) {
16156
16310
  return void 0;
16157
16311
  }
@@ -17108,11 +17262,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17108
17262
  const ext = FORMAT_EXTENSIONS[format];
17109
17263
  const effectiveName = outputName + (outputNameSuffix ?? "");
17110
17264
  if (mode === "aggregated") {
17111
- return toPosix(path4.join(baseOutputDir, `${effectiveName}${ext}`));
17265
+ return toPosix(path6.join(baseOutputDir, `${effectiveName}${ext}`));
17112
17266
  }
17113
17267
  const normalizedSource = toPosix(sourceFile);
17114
- const dirOfSource = path4.posix.dirname(normalizedSource);
17115
- let baseName = path4.posix.basename(normalizedSource);
17268
+ const dirOfSource = path6.posix.dirname(normalizedSource);
17269
+ let baseName = path6.posix.basename(normalizedSource);
17116
17270
  for (const testExt of TEST_EXTENSIONS) {
17117
17271
  if (baseName.endsWith(testExt)) {
17118
17272
  baseName = baseName.slice(0, -testExt.length);
@@ -17121,9 +17275,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17121
17275
  }
17122
17276
  const fileName = `${baseName}.${effectiveName}${ext}`;
17123
17277
  if (colocatedStyle === "adjacent") {
17124
- return toPosix(path4.posix.join(dirOfSource, fileName));
17278
+ return toPosix(path6.posix.join(dirOfSource, fileName));
17125
17279
  }
17126
- return toPosix(path4.posix.join(baseOutputDir, dirOfSource, fileName));
17280
+ return toPosix(path6.posix.join(baseOutputDir, dirOfSource, fileName));
17127
17281
  }
17128
17282
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
17129
17283
  const groups = /* @__PURE__ */ new Map();
@@ -17222,6 +17376,7 @@ var ReportGenerator = class {
17222
17376
  mermaidEnabled: options.html?.mermaidEnabled ?? true,
17223
17377
  markdownEnabled: options.html?.markdownEnabled ?? true,
17224
17378
  permalinkBaseUrl: options.html?.permalinkBaseUrl,
17379
+ ticketUrlTemplate: options.html?.ticketUrlTemplate,
17225
17380
  theme: options.html?.theme ?? "default"
17226
17381
  },
17227
17382
  junit: {
@@ -17245,7 +17400,9 @@ var ReportGenerator = class {
17245
17400
  traceUrlTemplate: options.markdown?.traceUrlTemplate,
17246
17401
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
17247
17402
  customRenderers: options.markdown?.customRenderers
17248
- }
17403
+ },
17404
+ assetMode: options.assetMode ?? "none",
17405
+ allowMissingAssets: options.allowMissingAssets ?? false
17249
17406
  };
17250
17407
  }
17251
17408
  /**
@@ -17272,6 +17429,16 @@ var ReportGenerator = class {
17272
17429
  const paths = await this.generateFormat(filteredRun, format);
17273
17430
  results.set(format, paths);
17274
17431
  }
17432
+ if (this.options.assetMode === "copy") {
17433
+ const htmlPaths = results.get("html");
17434
+ if (htmlPaths) {
17435
+ for (const htmlPath of htmlPaths) {
17436
+ bundleAssets(htmlPath, {
17437
+ allowMissing: this.options.allowMissingAssets
17438
+ });
17439
+ }
17440
+ }
17441
+ }
17275
17442
  return results;
17276
17443
  }
17277
17444
  /**
@@ -17289,9 +17456,9 @@ var ReportGenerator = class {
17289
17456
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
17290
17457
  const ext = FORMAT_EXTENSIONS[format];
17291
17458
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
17292
- const outputPath = toPosix(path4.join(this.options.outputDir, `${effectiveName}${ext}`));
17459
+ const outputPath = toPosix(path6.join(this.options.outputDir, `${effectiveName}${ext}`));
17293
17460
  const content = await this.formatContent(run, format);
17294
- const dir = path4.dirname(outputPath);
17461
+ const dir = path6.dirname(outputPath);
17295
17462
  await fsPromises.mkdir(dir, { recursive: true });
17296
17463
  await this.deps.writeFile(outputPath, content);
17297
17464
  return [outputPath];
@@ -17303,7 +17470,7 @@ var ReportGenerator = class {
17303
17470
  testCases
17304
17471
  };
17305
17472
  const content = await this.formatContent(groupRun, format);
17306
- const dir = path4.dirname(outputPath);
17473
+ const dir = path6.dirname(outputPath);
17307
17474
  await fsPromises.mkdir(dir, { recursive: true });
17308
17475
  await this.deps.writeFile(outputPath, content);
17309
17476
  writtenPaths.push(outputPath);
@@ -17332,7 +17499,8 @@ var ReportGenerator = class {
17332
17499
  syntaxHighlighting: this.options.html.syntaxHighlighting,
17333
17500
  mermaidEnabled: this.options.html.mermaidEnabled,
17334
17501
  markdownEnabled: this.options.html.markdownEnabled,
17335
- permalinkBaseUrl: this.options.html.permalinkBaseUrl
17502
+ permalinkBaseUrl: this.options.html.permalinkBaseUrl,
17503
+ ticketUrlTemplate: this.options.html.ticketUrlTemplate
17336
17504
  });
17337
17505
  return formatter.format(run);
17338
17506
  }
@@ -17400,7 +17568,7 @@ async function generateRunComparison(args) {
17400
17568
  await fsPromises.mkdir(outputDir, { recursive: true });
17401
17569
  for (const format of args.formats) {
17402
17570
  const ext = format === "html" ? ".html" : ".md";
17403
- const outputPath = toPosix(path4.join(outputDir, `${outputName}${ext}`));
17571
+ const outputPath = toPosix(path6.join(outputDir, `${outputName}${ext}`));
17404
17572
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
17405
17573
  await fsPromises.writeFile(outputPath, content, "utf8");
17406
17574
  files.push(outputPath);
@@ -17437,6 +17605,7 @@ export {
17437
17605
  adaptPlaywrightRun,
17438
17606
  adaptVitestRun,
17439
17607
  assertValidRun,
17608
+ bundleAssets,
17440
17609
  calculateFlakiness,
17441
17610
  calculateStability,
17442
17611
  canonicalizeRun,