executable-stories-formatters 0.7.7 → 0.7.9

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
@@ -32,8 +32,8 @@ function generateRunId(startedAtMs, projectRoot) {
32
32
  const input = `${startedAtMs}::${projectRoot}`;
33
33
  return createHash("sha1").update(input).digest("hex").slice(0, 16);
34
34
  }
35
- function slugify(text) {
36
- return text.toLowerCase().replace(/[/\\]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
35
+ function slugify(text2) {
36
+ return text2.toLowerCase().replace(/[/\\]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
37
37
  }
38
38
  function generateFeatureId(uri) {
39
39
  const pathWithoutExt = uri.replace(/\.[^.]+$/, "");
@@ -13022,7 +13022,7 @@ function renderDocEntry(entry, deps) {
13022
13022
  // src/formatters/html/renderers/steps.ts
13023
13023
  var CONTINUATION_KEYWORDS = ["And", "But", "*"];
13024
13024
  function renderStep(step, stepResult, index, deps) {
13025
- const statusIcon = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
13025
+ const statusIcon2 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
13026
13026
  const statusClass = stepResult ? `status-${stepResult.status}` : "";
13027
13027
  const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
13028
13028
  const keywordTrimmed = step.keyword.trim();
@@ -13031,7 +13031,7 @@ function renderStep(step, stepResult, index, deps) {
13031
13031
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
13032
13032
  const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
13033
13033
  return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
13034
- <span class="step-status ${statusClass}">${statusIcon}</span>
13034
+ <span class="step-status ${statusClass}">${statusIcon2}</span>
13035
13035
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
13036
13036
  <span class="step-text">${textHtml}</span>
13037
13037
  <span class="step-duration">${duration}</span>
@@ -13047,10 +13047,10 @@ function renderSteps(args, deps) {
13047
13047
 
13048
13048
  // src/formatters/html/renderers/step-params.ts
13049
13049
  var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
13050
- function highlightStepParams(text, deps) {
13051
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
13050
+ function highlightStepParams(text2, deps) {
13051
+ const matches = Array.from(text2.matchAll(STEP_PARAM_PATTERN));
13052
13052
  if (matches.length === 0) {
13053
- return deps.escapeHtml(text);
13053
+ return deps.escapeHtml(text2);
13054
13054
  }
13055
13055
  let result = "";
13056
13056
  let lastIndex = 0;
@@ -13058,13 +13058,13 @@ function highlightStepParams(text, deps) {
13058
13058
  const matchStart = match.index;
13059
13059
  const matchEnd = matchStart + match[0].length;
13060
13060
  if (matchStart > lastIndex) {
13061
- result += deps.escapeHtml(text.slice(lastIndex, matchStart));
13061
+ result += deps.escapeHtml(text2.slice(lastIndex, matchStart));
13062
13062
  }
13063
13063
  result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
13064
13064
  lastIndex = matchEnd;
13065
13065
  }
13066
- if (lastIndex < text.length) {
13067
- result += deps.escapeHtml(text.slice(lastIndex));
13066
+ if (lastIndex < text2.length) {
13067
+ result += deps.escapeHtml(text2.slice(lastIndex));
13068
13068
  }
13069
13069
  return result;
13070
13070
  }
@@ -13087,7 +13087,7 @@ function renderTicket(ticket, template, escapeHtml3) {
13087
13087
  }
13088
13088
  function renderScenario(args, deps) {
13089
13089
  const { tc } = args;
13090
- const statusIcon = deps.getStatusIcon(tc.status);
13090
+ const statusIcon2 = deps.getStatusIcon(tc.status);
13091
13091
  const statusClass = `status-${tc.status}`;
13092
13092
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13093
13093
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
@@ -13156,7 +13156,7 @@ function renderScenario(args, deps) {
13156
13156
  <div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
13157
13157
  <div class="scenario-info">
13158
13158
  <div class="scenario-title">
13159
- <span class="status-icon ${statusClass}">${statusIcon}</span>
13159
+ <span class="status-icon ${statusClass}">${statusIcon2}</span>
13160
13160
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13161
13161
  </div>
13162
13162
  <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
@@ -13295,11 +13295,11 @@ function buildTooltip(span, escapeHtml3) {
13295
13295
  parts.push(`${key}=${formatted}`);
13296
13296
  }
13297
13297
  }
13298
- let text = parts.join("\n");
13299
- if (text.length > TOOLTIP_MAX_LENGTH) {
13300
- text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
13298
+ let text2 = parts.join("\n");
13299
+ if (text2.length > TOOLTIP_MAX_LENGTH) {
13300
+ text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
13301
13301
  }
13302
- return escapeHtml3(text);
13302
+ return escapeHtml3(text2);
13303
13303
  }
13304
13304
  function renderTraceView(args, deps) {
13305
13305
  if (!args.spans || args.spans.length === 0) return "";
@@ -13522,11 +13522,11 @@ function renderToc(args, deps) {
13522
13522
  const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
13523
13523
  const featureSlug = `feature-${slugify(file)}`;
13524
13524
  const scenarios = testCases.map((tc) => {
13525
- const statusIcon = deps.getStatusIcon(tc.status);
13525
+ const statusIcon2 = deps.getStatusIcon(tc.status);
13526
13526
  const statusClass = `status-${tc.status}`;
13527
13527
  const failedClass = tc.status === "failed" ? " toc-failed" : "";
13528
13528
  return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
13529
- <span class="toc-status ${statusClass}">${statusIcon}</span>
13529
+ <span class="toc-status ${statusClass}">${statusIcon2}</span>
13530
13530
  ${deps.escapeHtml(tc.story.scenario)}
13531
13531
  </a>`;
13532
13532
  }).join("\n");
@@ -13584,7 +13584,7 @@ function createHtmlFormatter(options = {}) {
13584
13584
  escapeHtml,
13585
13585
  getStatusIcon,
13586
13586
  renderDocs,
13587
- highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
13587
+ highlightStepParams: (text2) => highlightStepParams(text2, { escapeHtml })
13588
13588
  };
13589
13589
  const scenarioDeps = {
13590
13590
  escapeHtml,
@@ -14519,7 +14519,7 @@ function deterministicId(kind, salt, ...parts) {
14519
14519
 
14520
14520
  // src/formatters/cucumber-messages/build-gherkin-document.ts
14521
14521
  function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
14522
- const { lineMap, featureName, featureTags, text } = synthesized;
14522
+ const { lineMap, featureName, featureTags, text: text2 } = synthesized;
14523
14523
  const featureTagNodes = featureTags.map((tag, i) => ({
14524
14524
  location: {
14525
14525
  line: lineMap.featureTagLine ?? 1,
@@ -14588,7 +14588,7 @@ function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
14588
14588
  sourceEnvelope: {
14589
14589
  source: {
14590
14590
  uri,
14591
- data: text,
14591
+ data: text2,
14592
14592
  mediaType: "text/x.cucumber.gherkin+plain"
14593
14593
  }
14594
14594
  },
@@ -14599,8 +14599,8 @@ function buildStepArguments(step, stepLine) {
14599
14599
  if (!step.docs || step.docs.length === 0) return {};
14600
14600
  const tableDocs = step.docs.filter((d) => d.kind === "table");
14601
14601
  if (tableDocs.length > 0) {
14602
- const table = tableDocs[0];
14603
- return { dataTable: buildDataTable(table, stepLine + 1) };
14602
+ const table2 = tableDocs[0];
14603
+ return { dataTable: buildDataTable(table2, stepLine + 1) };
14604
14604
  }
14605
14605
  for (const doc of step.docs) {
14606
14606
  const ds = docEntryToDocString(doc, stepLine + 1);
@@ -14671,21 +14671,21 @@ function docEntryToDocString(doc, line) {
14671
14671
  return void 0;
14672
14672
  }
14673
14673
  }
14674
- function buildDataTable(table, line) {
14674
+ function buildDataTable(table2, line) {
14675
14675
  const rows = [];
14676
14676
  rows.push({
14677
14677
  location: { line },
14678
- cells: table.columns.map((col) => ({
14678
+ cells: table2.columns.map((col) => ({
14679
14679
  location: { line },
14680
14680
  value: col
14681
14681
  })),
14682
14682
  id: ""
14683
14683
  });
14684
- for (let r = 0; r < table.rows.length; r++) {
14684
+ for (let r = 0; r < table2.rows.length; r++) {
14685
14685
  const rowLine = line + 1 + r;
14686
14686
  rows.push({
14687
14687
  location: { line: rowLine },
14688
- cells: table.rows[r].map((cell) => ({
14688
+ cells: table2.rows[r].map((cell) => ({
14689
14689
  location: { line: rowLine },
14690
14690
  value: cell
14691
14691
  })),
@@ -14776,12 +14776,12 @@ function docEntryToPickleDocString(doc) {
14776
14776
  return void 0;
14777
14777
  }
14778
14778
  }
14779
- function buildPickleTable(table) {
14779
+ function buildPickleTable(table2) {
14780
14780
  const rows = [];
14781
14781
  rows.push({
14782
- cells: table.columns.map((col) => ({ value: col }))
14782
+ cells: table2.columns.map((col) => ({ value: col }))
14783
14783
  });
14784
- for (const row of table.rows) {
14784
+ for (const row of table2.rows) {
14785
14785
  rows.push({
14786
14786
  cells: row.map((cell) => ({ value: cell }))
14787
14787
  });
@@ -16197,6 +16197,414 @@ ${body}`;
16197
16197
  }
16198
16198
  };
16199
16199
 
16200
+ // src/formatters/confluence.ts
16201
+ var ConfluenceFormatter = class {
16202
+ options;
16203
+ constructor(options = {}) {
16204
+ this.options = {
16205
+ title: options.title ?? "User Stories",
16206
+ includeStatusIcons: options.includeStatusIcons ?? true,
16207
+ includeMetadata: options.includeMetadata ?? true,
16208
+ includeSummaryTable: options.includeSummaryTable ?? true,
16209
+ includeErrors: options.includeErrors ?? true,
16210
+ scenarioHeadingLevel: options.scenarioHeadingLevel ?? 3,
16211
+ groupBy: options.groupBy ?? "file",
16212
+ sortScenarios: options.sortScenarios ?? "source",
16213
+ pretty: options.pretty ?? true,
16214
+ permalinkBaseUrl: options.permalinkBaseUrl,
16215
+ ticketUrlTemplate: options.ticketUrlTemplate
16216
+ };
16217
+ }
16218
+ /** Build the ADF document tree. Returns the JS object (not stringified). */
16219
+ formatToAdf(run) {
16220
+ const content = [];
16221
+ content.push(heading(1, [text(this.options.title)]));
16222
+ if (this.options.includeMetadata) {
16223
+ const metaTable = this.renderMetadataTable(run);
16224
+ if (metaTable) content.push(metaTable);
16225
+ }
16226
+ if (this.options.includeSummaryTable) {
16227
+ content.push(this.renderSummaryTable(run));
16228
+ }
16229
+ switch (this.options.groupBy) {
16230
+ case "none":
16231
+ this.renderFlat(content, run.testCases);
16232
+ break;
16233
+ case "suite":
16234
+ this.renderBySuite(content, run.testCases);
16235
+ break;
16236
+ case "file":
16237
+ default:
16238
+ this.renderByFile(content, run.testCases);
16239
+ break;
16240
+ }
16241
+ return { version: 1, type: "doc", content };
16242
+ }
16243
+ /** Format a test run as an ADF JSON string. */
16244
+ format(run) {
16245
+ const adf = this.formatToAdf(run);
16246
+ return this.options.pretty ? JSON.stringify(adf, null, 2) : JSON.stringify(adf);
16247
+ }
16248
+ // --------------------------------------------------------------------------
16249
+ // Metadata / summary tables
16250
+ // --------------------------------------------------------------------------
16251
+ renderMetadataTable(run) {
16252
+ const rows = [];
16253
+ rows.push(["Date", new Date(run.startedAtMs).toISOString()]);
16254
+ if (run.packageVersion) rows.push(["Version", run.packageVersion]);
16255
+ if (run.gitSha) {
16256
+ const shortSha = run.gitSha.length > 7 ? run.gitSha.slice(0, 7) : run.gitSha;
16257
+ rows.push(["Git SHA", shortSha]);
16258
+ }
16259
+ if (rows.length === 0) return null;
16260
+ return table([
16261
+ tableRow([tableHeader("Key"), tableHeader("Value")]),
16262
+ ...rows.map(([k, v]) => tableRow([tableCell(k), tableCell(v)]))
16263
+ ]);
16264
+ }
16265
+ renderSummaryTable(run) {
16266
+ const total = run.testCases.length;
16267
+ const steps = run.testCases.reduce(
16268
+ (acc, tc) => acc + tc.story.steps.length,
16269
+ 0
16270
+ );
16271
+ const passed = run.testCases.filter((tc) => tc.status === "passed").length;
16272
+ const failed = run.testCases.filter((tc) => tc.status === "failed").length;
16273
+ const skipped = run.testCases.filter((tc) => tc.status === "skipped").length;
16274
+ const pending = run.testCases.filter((tc) => tc.status === "pending").length;
16275
+ return table([
16276
+ tableRow([
16277
+ tableHeader("Scenarios"),
16278
+ tableHeader("Steps"),
16279
+ tableHeader("Passed"),
16280
+ tableHeader("Failed"),
16281
+ tableHeader("Skipped"),
16282
+ tableHeader("Pending"),
16283
+ tableHeader("Duration")
16284
+ ]),
16285
+ tableRow([
16286
+ tableCell(String(total)),
16287
+ tableCell(String(steps)),
16288
+ tableCell(String(passed)),
16289
+ tableCell(String(failed)),
16290
+ tableCell(String(skipped)),
16291
+ tableCell(String(pending)),
16292
+ tableCell(formatDuration2(run.durationMs))
16293
+ ])
16294
+ ]);
16295
+ }
16296
+ // --------------------------------------------------------------------------
16297
+ // Grouping
16298
+ // --------------------------------------------------------------------------
16299
+ renderByFile(content, testCases) {
16300
+ const byFile = groupBy7(testCases, (tc) => tc.sourceFile);
16301
+ for (const [file, fileCases] of byFile) {
16302
+ content.push(heading(2, [codeInline(file)]));
16303
+ this.renderSuiteGroups(content, fileCases, 3);
16304
+ }
16305
+ }
16306
+ renderBySuite(content, testCases) {
16307
+ this.renderSuiteGroups(content, testCases, 2);
16308
+ }
16309
+ renderFlat(content, testCases) {
16310
+ const sorted = this.sortCases(testCases);
16311
+ for (const tc of sorted) this.renderScenario(content, tc);
16312
+ }
16313
+ renderSuiteGroups(content, testCases, baseLevel) {
16314
+ const bySuite = groupBy7(testCases, (tc) => tc.titlePath.join(" - "));
16315
+ const entries = this.sortSuiteGroups([...bySuite.entries()]);
16316
+ for (const [suitePath, cases] of entries) {
16317
+ if (suitePath) {
16318
+ content.push(
16319
+ heading(clampHeadingLevel(baseLevel), [text(suitePath)])
16320
+ );
16321
+ }
16322
+ for (const tc of this.sortCases(cases)) {
16323
+ this.renderScenario(content, tc);
16324
+ }
16325
+ }
16326
+ }
16327
+ // --------------------------------------------------------------------------
16328
+ // Scenario
16329
+ // --------------------------------------------------------------------------
16330
+ renderScenario(content, tc) {
16331
+ const level = clampHeadingLevel(this.options.scenarioHeadingLevel);
16332
+ const headingNodes = [];
16333
+ if (this.options.includeStatusIcons) {
16334
+ headingNodes.push(text(`${statusIcon(tc.status)} `));
16335
+ }
16336
+ headingNodes.push(text(tc.story.scenario));
16337
+ content.push(heading(level, headingNodes));
16338
+ const metaChildren = [];
16339
+ if (tc.tags.length > 0) {
16340
+ metaChildren.push(text("Tags: ", strong()));
16341
+ tc.tags.forEach((t, i) => {
16342
+ if (i > 0) metaChildren.push(text(", "));
16343
+ metaChildren.push(codeInline(t));
16344
+ });
16345
+ }
16346
+ if (tc.story.tickets && tc.story.tickets.length > 0) {
16347
+ if (metaChildren.length > 0) metaChildren.push(text(" | "));
16348
+ metaChildren.push(text("Tickets: ", strong()));
16349
+ tc.story.tickets.forEach((ticket, i) => {
16350
+ if (i > 0) metaChildren.push(text(", "));
16351
+ const url = ticket.url ?? (this.options.ticketUrlTemplate ? this.options.ticketUrlTemplate.replace("{ticket}", ticket.id) : void 0);
16352
+ metaChildren.push(
16353
+ url ? link(ticket.id, url) : codeInline(ticket.id)
16354
+ );
16355
+ });
16356
+ }
16357
+ if (this.options.permalinkBaseUrl && tc.sourceFile !== "unknown" && tc.sourceFile) {
16358
+ if (metaChildren.length > 0) metaChildren.push(text(" | "));
16359
+ metaChildren.push(text("Source: ", strong()));
16360
+ const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
16361
+ const url = `${base}/${tc.sourceFile}${tc.sourceLine > 0 ? `#L${tc.sourceLine}` : ""}`;
16362
+ metaChildren.push(link(tc.sourceFile, url));
16363
+ }
16364
+ if (metaChildren.length > 0) {
16365
+ content.push(paragraph(metaChildren));
16366
+ }
16367
+ if (tc.story.docs && tc.story.docs.length > 0) {
16368
+ for (const doc of tc.story.docs) this.renderDocEntry(content, doc);
16369
+ }
16370
+ if (tc.story.steps.length > 0) {
16371
+ content.push(this.renderStepsList(tc.story.steps));
16372
+ for (const step of tc.story.steps) {
16373
+ if (step.docs && step.docs.length > 0) {
16374
+ for (const doc of step.docs) this.renderDocEntry(content, doc);
16375
+ }
16376
+ }
16377
+ }
16378
+ if (tc.status === "failed" && tc.errorMessage && this.options.includeErrors) {
16379
+ const errorContent = (tc.errorMessage ?? "") + (tc.errorStack ? `
16380
+
16381
+ ${tc.errorStack}` : "");
16382
+ content.push(
16383
+ panel("warning", [paragraph([text("Failure", strong())])])
16384
+ );
16385
+ content.push(codeBlock(errorContent, "text"));
16386
+ }
16387
+ }
16388
+ renderStepsList(steps) {
16389
+ return {
16390
+ type: "bulletList",
16391
+ content: steps.map((step) => {
16392
+ const children = [text(`${step.keyword} `, strong()), text(step.text)];
16393
+ if (step.mode && step.mode !== "normal") {
16394
+ children.push(text(` (${step.mode})`, em()));
16395
+ }
16396
+ return {
16397
+ type: "listItem",
16398
+ content: [paragraph(children)]
16399
+ };
16400
+ })
16401
+ };
16402
+ }
16403
+ // --------------------------------------------------------------------------
16404
+ // Doc entries
16405
+ // --------------------------------------------------------------------------
16406
+ renderDocEntry(content, entry) {
16407
+ switch (entry.kind) {
16408
+ case "note":
16409
+ content.push(panel("info", [paragraph([text(entry.text)])]));
16410
+ break;
16411
+ case "tag": {
16412
+ const kids = [];
16413
+ entry.names.forEach((name, i) => {
16414
+ if (i > 0) kids.push(text(" "));
16415
+ kids.push(codeInline(name));
16416
+ });
16417
+ if (kids.length > 0) content.push(paragraph(kids));
16418
+ break;
16419
+ }
16420
+ case "kv": {
16421
+ const val = typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value);
16422
+ content.push(
16423
+ paragraph([text(`${entry.label}: `, strong()), codeInline(val)])
16424
+ );
16425
+ break;
16426
+ }
16427
+ case "code":
16428
+ if (entry.label) {
16429
+ content.push(paragraph([text(entry.label, strong())]));
16430
+ }
16431
+ content.push(codeBlock(entry.content ?? "", entry.lang));
16432
+ break;
16433
+ case "table":
16434
+ if (entry.label) {
16435
+ content.push(paragraph([text(entry.label, strong())]));
16436
+ }
16437
+ content.push(
16438
+ table([
16439
+ tableRow(entry.columns.map((c) => tableHeader(c))),
16440
+ ...entry.rows.map(
16441
+ (row) => tableRow(row.map((cell) => tableCell(cell)))
16442
+ )
16443
+ ])
16444
+ );
16445
+ break;
16446
+ case "link":
16447
+ content.push(paragraph([link(entry.label, entry.url)]));
16448
+ break;
16449
+ case "section":
16450
+ if (entry.title) {
16451
+ content.push(paragraph([text(entry.title, strong())]));
16452
+ }
16453
+ if (entry.markdown) {
16454
+ for (const para of entry.markdown.split(/\n{2,}/)) {
16455
+ const trimmed = para.trim();
16456
+ if (trimmed) content.push(paragraph([text(trimmed)]));
16457
+ }
16458
+ }
16459
+ break;
16460
+ case "mermaid":
16461
+ if (entry.title) {
16462
+ content.push(paragraph([text(entry.title, strong())]));
16463
+ }
16464
+ content.push(codeBlock(entry.code ?? "", "mermaid"));
16465
+ break;
16466
+ case "screenshot":
16467
+ content.push(
16468
+ paragraph([
16469
+ text(entry.alt ?? "Screenshot", strong()),
16470
+ text(": "),
16471
+ link(entry.path, entry.path)
16472
+ ])
16473
+ );
16474
+ break;
16475
+ case "custom":
16476
+ content.push(paragraph([text(`[${entry.type}]`, strong())]));
16477
+ content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
16478
+ break;
16479
+ }
16480
+ if (entry.children && entry.children.length > 0) {
16481
+ for (const child of entry.children) {
16482
+ this.renderDocEntry(content, child);
16483
+ }
16484
+ }
16485
+ }
16486
+ // --------------------------------------------------------------------------
16487
+ // Sorting
16488
+ // --------------------------------------------------------------------------
16489
+ sortCases(cases) {
16490
+ if (this.options.sortScenarios === "alpha") {
16491
+ return [...cases].sort(
16492
+ (a, b) => a.story.scenario.localeCompare(b.story.scenario)
16493
+ );
16494
+ }
16495
+ if (this.options.sortScenarios === "source") {
16496
+ return [...cases].sort(
16497
+ (a, b) => (a.story.sourceOrder ?? 0) - (b.story.sourceOrder ?? 0)
16498
+ );
16499
+ }
16500
+ return cases;
16501
+ }
16502
+ sortSuiteGroups(entries) {
16503
+ if (this.options.sortScenarios === "alpha") {
16504
+ return entries.sort(([a], [b]) => a.localeCompare(b));
16505
+ }
16506
+ if (this.options.sortScenarios === "source") {
16507
+ return entries.sort(([, a], [, b]) => {
16508
+ const minA = Math.min(...a.map((s) => s.story.sourceOrder ?? Infinity));
16509
+ const minB = Math.min(...b.map((s) => s.story.sourceOrder ?? Infinity));
16510
+ return minA - minB;
16511
+ });
16512
+ }
16513
+ return entries;
16514
+ }
16515
+ };
16516
+ function text(value, mark) {
16517
+ const node = { type: "text", text: value };
16518
+ if (mark) {
16519
+ node.marks = Array.isArray(mark) ? mark : [mark];
16520
+ }
16521
+ return node;
16522
+ }
16523
+ function strong() {
16524
+ return { type: "strong" };
16525
+ }
16526
+ function em() {
16527
+ return { type: "em" };
16528
+ }
16529
+ function codeMark() {
16530
+ return { type: "code" };
16531
+ }
16532
+ function codeInline(value) {
16533
+ return text(value, codeMark());
16534
+ }
16535
+ function link(label, href) {
16536
+ return text(label, { type: "link", attrs: { href } });
16537
+ }
16538
+ function paragraph(content) {
16539
+ return { type: "paragraph", content };
16540
+ }
16541
+ function heading(level, content) {
16542
+ return {
16543
+ type: "heading",
16544
+ attrs: { level: clampHeadingLevel(level) },
16545
+ content
16546
+ };
16547
+ }
16548
+ function codeBlock(content, lang) {
16549
+ return {
16550
+ type: "codeBlock",
16551
+ attrs: lang ? { language: lang } : {},
16552
+ content: content ? [{ type: "text", text: content }] : []
16553
+ };
16554
+ }
16555
+ function panel(panelType, content) {
16556
+ return { type: "panel", attrs: { panelType }, content };
16557
+ }
16558
+ function table(rows) {
16559
+ return {
16560
+ type: "table",
16561
+ attrs: { isNumberColumnEnabled: false, layout: "default" },
16562
+ content: rows
16563
+ };
16564
+ }
16565
+ function tableRow(cells) {
16566
+ return { type: "tableRow", content: cells };
16567
+ }
16568
+ function tableHeader(value) {
16569
+ return { type: "tableHeader", content: [paragraph([text(value)])] };
16570
+ }
16571
+ function tableCell(value) {
16572
+ return { type: "tableCell", content: [paragraph([text(value)])] };
16573
+ }
16574
+ function clampHeadingLevel(level) {
16575
+ if (level < 1) return 1;
16576
+ if (level > 6) return 6;
16577
+ return level;
16578
+ }
16579
+ function statusIcon(status) {
16580
+ switch (status) {
16581
+ case "passed":
16582
+ return "\u2705";
16583
+ case "failed":
16584
+ return "\u274C";
16585
+ case "skipped":
16586
+ return "\u23E9";
16587
+ case "pending":
16588
+ return "\u{1F4DD}";
16589
+ default:
16590
+ return "\u26A0\uFE0F";
16591
+ }
16592
+ }
16593
+ function formatDuration2(ms) {
16594
+ if (ms < 1e3) return `${ms}ms`;
16595
+ return `${(ms / 1e3).toFixed(2)}s`;
16596
+ }
16597
+ function groupBy7(items, keyFn) {
16598
+ const map = /* @__PURE__ */ new Map();
16599
+ for (const item of items) {
16600
+ const key = keyFn(item);
16601
+ const existing = map.get(key);
16602
+ if (existing) existing.push(item);
16603
+ else map.set(key, [item]);
16604
+ }
16605
+ return map;
16606
+ }
16607
+
16200
16608
  // src/formatters/astro-assets.ts
16201
16609
  import * as fs4 from "fs";
16202
16610
  import * as path4 from "path";
@@ -16722,6 +17130,235 @@ ${result.errors.join("\n")}`);
16722
17130
  }
16723
17131
  }
16724
17132
 
17133
+ // src/publishers/confluence.ts
17134
+ function parseAdf(adf) {
17135
+ let parsed;
17136
+ try {
17137
+ parsed = JSON.parse(adf);
17138
+ } catch (err) {
17139
+ throw new Error(
17140
+ `ADF payload is not valid JSON: ${err.message}`
17141
+ );
17142
+ }
17143
+ if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
17144
+ throw new Error(
17145
+ `ADF payload must be an object with { version, type: "doc", content: [...] }`
17146
+ );
17147
+ }
17148
+ return parsed;
17149
+ }
17150
+ function basicAuthHeader(auth) {
17151
+ const raw = `${auth.email}:${auth.token}`;
17152
+ const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
17153
+ return `Basic ${encoded}`;
17154
+ }
17155
+ async function parseErrorBody(response) {
17156
+ try {
17157
+ const body = await response.text();
17158
+ return body ? body.slice(0, 800) : "";
17159
+ } catch {
17160
+ return "";
17161
+ }
17162
+ }
17163
+ async function publishConfluencePage(args, deps) {
17164
+ parseAdf(args.adf);
17165
+ if (!args.pageId && !args.spaceId) {
17166
+ throw new Error(
17167
+ "publishConfluencePage requires either pageId (update) or spaceId (create)"
17168
+ );
17169
+ }
17170
+ if (!args.pageId && !args.title) {
17171
+ throw new Error("Creating a new page requires a title");
17172
+ }
17173
+ const base = args.baseUrl.replace(/\/$/, "");
17174
+ const fetchFn = deps.fetch ?? globalThis.fetch;
17175
+ if (!fetchFn) {
17176
+ throw new Error("No fetch implementation available (Node >= 22 expected)");
17177
+ }
17178
+ const headers = {
17179
+ Authorization: basicAuthHeader(deps.auth),
17180
+ Accept: "application/json",
17181
+ "Content-Type": "application/json"
17182
+ };
17183
+ if (args.pageId) {
17184
+ return updatePage(args, base, headers, fetchFn);
17185
+ }
17186
+ return createPage(args, base, headers, fetchFn);
17187
+ }
17188
+ async function updatePage(args, base, headers, fetchFn) {
17189
+ const getUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
17190
+ const getResp = await fetchFn(getUrl, { method: "GET", headers });
17191
+ if (!getResp.ok) {
17192
+ const body = await parseErrorBody(getResp);
17193
+ throw new Error(
17194
+ `GET ${getUrl} failed with ${getResp.status} ${getResp.statusText}${body ? `: ${body}` : ""}`
17195
+ );
17196
+ }
17197
+ const current = await getResp.json();
17198
+ const nextVersion = current.version.number + 1;
17199
+ const title = args.title ?? current.title;
17200
+ const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
17201
+ const putResp = await fetchFn(putUrl, {
17202
+ method: "PUT",
17203
+ headers,
17204
+ body: JSON.stringify({
17205
+ id: args.pageId,
17206
+ status: "current",
17207
+ title,
17208
+ body: {
17209
+ representation: "atlas_doc_format",
17210
+ value: args.adf
17211
+ },
17212
+ version: { number: nextVersion }
17213
+ })
17214
+ });
17215
+ if (!putResp.ok) {
17216
+ const body = await parseErrorBody(putResp);
17217
+ throw new Error(
17218
+ `PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
17219
+ );
17220
+ }
17221
+ const updated = await putResp.json();
17222
+ return {
17223
+ id: updated.id,
17224
+ title: updated.title,
17225
+ version: updated.version.number,
17226
+ url: buildPageUrl(base, updated._links?.webui, updated.id),
17227
+ action: "updated"
17228
+ };
17229
+ }
17230
+ async function createPage(args, base, headers, fetchFn) {
17231
+ const body = {
17232
+ spaceId: args.spaceId,
17233
+ status: "current",
17234
+ title: args.title,
17235
+ body: {
17236
+ representation: "atlas_doc_format",
17237
+ value: args.adf
17238
+ }
17239
+ };
17240
+ if (args.parentId) body.parentId = args.parentId;
17241
+ const postUrl = `${base}/api/v2/pages`;
17242
+ const resp = await fetchFn(postUrl, {
17243
+ method: "POST",
17244
+ headers,
17245
+ body: JSON.stringify(body)
17246
+ });
17247
+ if (!resp.ok) {
17248
+ const errBody = await parseErrorBody(resp);
17249
+ throw new Error(
17250
+ `POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
17251
+ );
17252
+ }
17253
+ const created = await resp.json();
17254
+ return {
17255
+ id: created.id,
17256
+ title: created.title,
17257
+ version: created.version.number,
17258
+ url: buildPageUrl(base, created._links?.webui, created.id),
17259
+ action: "created"
17260
+ };
17261
+ }
17262
+ function buildPageUrl(base, webui, id) {
17263
+ if (webui) {
17264
+ return webui.startsWith("http") ? webui : `${base}${webui}`;
17265
+ }
17266
+ return `${base}/pages/${id}`;
17267
+ }
17268
+
17269
+ // src/publishers/jira.ts
17270
+ function parseAdf2(adf) {
17271
+ let parsed;
17272
+ try {
17273
+ parsed = JSON.parse(adf);
17274
+ } catch (err) {
17275
+ throw new Error(
17276
+ `ADF payload is not valid JSON: ${err.message}`
17277
+ );
17278
+ }
17279
+ if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
17280
+ throw new Error(
17281
+ `ADF payload must be an object with { version, type: "doc", content: [...] }`
17282
+ );
17283
+ }
17284
+ return parsed;
17285
+ }
17286
+ function basicAuthHeader2(auth) {
17287
+ const raw = `${auth.email}:${auth.token}`;
17288
+ const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
17289
+ return `Basic ${encoded}`;
17290
+ }
17291
+ async function parseErrorBody2(resp) {
17292
+ try {
17293
+ const body = await resp.text();
17294
+ return body ? body.slice(0, 800) : "";
17295
+ } catch {
17296
+ return "";
17297
+ }
17298
+ }
17299
+ async function publishJiraIssue(args, deps) {
17300
+ const adfObject = parseAdf2(args.adf);
17301
+ if (!args.issueKey) {
17302
+ throw new Error("publishJiraIssue requires an issueKey, e.g. PROJ-123");
17303
+ }
17304
+ const base = args.baseUrl.replace(/\/$/, "");
17305
+ const fetchFn = deps.fetch ?? globalThis.fetch;
17306
+ if (!fetchFn) {
17307
+ throw new Error("No fetch implementation available (Node >= 22 expected)");
17308
+ }
17309
+ const headers = {
17310
+ Authorization: basicAuthHeader2(deps.auth),
17311
+ Accept: "application/json",
17312
+ "Content-Type": "application/json"
17313
+ };
17314
+ const mode = args.mode ?? "comment";
17315
+ if (mode === "description") {
17316
+ return updateDescription(args.issueKey, base, adfObject, headers, fetchFn);
17317
+ }
17318
+ return addComment(args.issueKey, base, adfObject, headers, fetchFn);
17319
+ }
17320
+ async function addComment(issueKey, base, adf, headers, fetchFn) {
17321
+ const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}/comment`;
17322
+ const resp = await fetchFn(url, {
17323
+ method: "POST",
17324
+ headers,
17325
+ body: JSON.stringify({ body: adf })
17326
+ });
17327
+ if (!resp.ok) {
17328
+ const body = await parseErrorBody2(resp);
17329
+ throw new Error(
17330
+ `POST ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
17331
+ );
17332
+ }
17333
+ const comment = await resp.json();
17334
+ const issueUrl = `${base}/browse/${encodeURIComponent(issueKey)}`;
17335
+ return {
17336
+ issueKey,
17337
+ action: "comment-added",
17338
+ url: `${issueUrl}?focusedCommentId=${encodeURIComponent(comment.id)}`,
17339
+ commentId: comment.id
17340
+ };
17341
+ }
17342
+ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
17343
+ const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}`;
17344
+ const resp = await fetchFn(url, {
17345
+ method: "PUT",
17346
+ headers,
17347
+ body: JSON.stringify({ fields: { description: adf } })
17348
+ });
17349
+ if (!resp.ok) {
17350
+ const body = await parseErrorBody2(resp);
17351
+ throw new Error(
17352
+ `PUT ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
17353
+ );
17354
+ }
17355
+ return {
17356
+ issueKey,
17357
+ action: "description-updated",
17358
+ url: `${base}/browse/${encodeURIComponent(issueKey)}`
17359
+ };
17360
+ }
17361
+
16725
17362
  // src/converters/ndjson-parser.ts
16726
17363
  function parseNdjson(ndjson) {
16727
17364
  const lines = ndjson.trim().split("\n").filter(Boolean);
@@ -17022,10 +17659,10 @@ function pickleStepArgumentToDocs(ps) {
17022
17659
  const docs = [];
17023
17660
  const phase = "static";
17024
17661
  if (ps.argument.dataTable) {
17025
- const table = ps.argument.dataTable;
17026
- if (table.rows.length > 0) {
17027
- const columns = table.rows[0].cells.map((c) => c.value);
17028
- const rows = table.rows.slice(1).map((r) => r.cells.map((c) => c.value));
17662
+ const table2 = ps.argument.dataTable;
17663
+ if (table2.rows.length > 0) {
17664
+ const columns = table2.rows[0].cells.map((c) => c.value);
17665
+ const rows = table2.rows.slice(1).map((r) => r.cells.map((c) => c.value));
17029
17666
  docs.push({
17030
17667
  kind: "table",
17031
17668
  label: "",
@@ -17160,7 +17797,7 @@ function readBranchName(cwd = process.cwd()) {
17160
17797
  }
17161
17798
 
17162
17799
  // src/utils/duration.ts
17163
- function formatDuration2(ms) {
17800
+ function formatDuration3(ms) {
17164
17801
  if (ms < 1e3) {
17165
17802
  return `${Math.round(ms)} ms`;
17166
17803
  }
@@ -17347,16 +17984,16 @@ function resolveTraceUrl(template, traceId) {
17347
17984
  }
17348
17985
 
17349
17986
  // src/notifiers/ansi-strip.ts
17350
- function stripAnsi(text) {
17351
- return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
17987
+ function stripAnsi(text2) {
17988
+ return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
17352
17989
  }
17353
17990
 
17354
17991
  // src/notifiers/slack.ts
17355
- function truncate(text, maxLen) {
17356
- if (text.length <= maxLen) return text;
17357
- return text.slice(0, maxLen - 3) + "...";
17992
+ function truncate(text2, maxLen) {
17993
+ if (text2.length <= maxLen) return text2;
17994
+ return text2.slice(0, maxLen - 3) + "...";
17358
17995
  }
17359
- function formatDuration3(ms) {
17996
+ function formatDuration4(ms) {
17360
17997
  const seconds = ms / 1e3;
17361
17998
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
17362
17999
  const minutes = Math.floor(seconds / 60);
@@ -17383,7 +18020,7 @@ function buildSlackPayload(summary, maxFailedTests) {
17383
18020
  { type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
17384
18021
  { type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
17385
18022
  { type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
17386
- { type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
18023
+ { type: "mrkdwn", text: `*Duration:* ${formatDuration4(summary.durationMs)}` },
17387
18024
  { type: "mrkdwn", text: `*Status:* ${statusText}` }
17388
18025
  ]
17389
18026
  });
@@ -17398,9 +18035,9 @@ function buildSlackPayload(summary, maxFailedTests) {
17398
18035
  }
17399
18036
  return `*${name}*`;
17400
18037
  });
17401
- let text = lines.join("\n\n");
18038
+ let text2 = lines.join("\n\n");
17402
18039
  if (summary.failedTests.length > maxFailedTests) {
17403
- text += `
18040
+ text2 += `
17404
18041
 
17405
18042
  _...and ${summary.failedTests.length - maxFailedTests} more_`;
17406
18043
  }
@@ -17408,7 +18045,7 @@ _...and ${summary.failedTests.length - maxFailedTests} more_`;
17408
18045
  type: "section",
17409
18046
  text: {
17410
18047
  type: "mrkdwn",
17411
- text
18048
+ text: text2
17412
18049
  }
17413
18050
  });
17414
18051
  }
@@ -17485,11 +18122,11 @@ async function sendSlackNotification(args, deps) {
17485
18122
  }
17486
18123
 
17487
18124
  // src/notifiers/teams.ts
17488
- function truncate2(text, maxLen) {
17489
- if (text.length <= maxLen) return text;
17490
- return text.slice(0, maxLen - 3) + "...";
18125
+ function truncate2(text2, maxLen) {
18126
+ if (text2.length <= maxLen) return text2;
18127
+ return text2.slice(0, maxLen - 3) + "...";
17491
18128
  }
17492
- function formatDuration4(ms) {
18129
+ function formatDuration5(ms) {
17493
18130
  const seconds = ms / 1e3;
17494
18131
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
17495
18132
  const minutes = Math.floor(seconds / 60);
@@ -17515,7 +18152,7 @@ function buildTeamsPayload(summary, maxFailedTests) {
17515
18152
  { title: "Passed", value: String(summary.passed) },
17516
18153
  { title: "Failed", value: String(summary.failed) },
17517
18154
  { title: "Skipped", value: String(summary.skipped) },
17518
- { title: "Duration", value: formatDuration4(summary.durationMs) }
18155
+ { title: "Duration", value: formatDuration5(summary.durationMs) }
17519
18156
  ]
17520
18157
  });
17521
18158
  if (summary.failedTests.length > 0) {
@@ -18091,6 +18728,37 @@ function listScenarios(args, _deps) {
18091
18728
  }));
18092
18729
  return JSON.stringify(items, null, 2);
18093
18730
  }
18731
+ if (format === "csv") {
18732
+ const header = "id,scenario,status,sourceFile,sourceLine,tags";
18733
+ const rows = testCases.map((tc) => {
18734
+ const fields = [
18735
+ tc.id,
18736
+ tc.story.scenario,
18737
+ tc.status,
18738
+ tc.sourceFile,
18739
+ String(tc.sourceLine),
18740
+ tc.tags.join(" ")
18741
+ ];
18742
+ return fields.map((f) => {
18743
+ if (f.includes(",") || f.includes('"') || f.includes("\n")) {
18744
+ return `"${f.replace(/"/g, '""')}"`;
18745
+ }
18746
+ return f;
18747
+ }).join(",");
18748
+ });
18749
+ return [header, ...rows].join("\n");
18750
+ }
18751
+ if (format === "markdown-table") {
18752
+ const header = "| Status | Scenario | Location | Tags |";
18753
+ const divider = "|--------|----------|----------|------|";
18754
+ const rows = testCases.map((tc) => {
18755
+ const icon = STATUS_ICONS[tc.status] ?? "?";
18756
+ const location = `${tc.sourceFile}:${tc.sourceLine}`;
18757
+ const tags = tc.tags.map((t) => `@${t}`).join(" ");
18758
+ return `| ${icon} | ${tc.story.scenario} | ${location} | ${tags} |`;
18759
+ });
18760
+ return [header, divider, ...rows].join("\n");
18761
+ }
18094
18762
  if (testCases.length === 0) {
18095
18763
  return "No scenarios found.";
18096
18764
  }
@@ -18138,7 +18806,8 @@ var FORMAT_EXTENSIONS = {
18138
18806
  "cucumber-html": ".cucumber.html",
18139
18807
  junit: ".junit.xml",
18140
18808
  "cucumber-json": ".cucumber.json",
18141
- "cucumber-messages": ".ndjson"
18809
+ "cucumber-messages": ".ndjson",
18810
+ confluence: ".adf.json"
18142
18811
  };
18143
18812
  var TEST_EXTENSIONS = [
18144
18813
  ".test.ts",
@@ -18306,6 +18975,19 @@ var ReportGenerator = class {
18306
18975
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
18307
18976
  customRenderers: options.markdown?.customRenderers
18308
18977
  },
18978
+ confluence: {
18979
+ title: options.confluence?.title ?? "User Stories",
18980
+ includeStatusIcons: options.confluence?.includeStatusIcons ?? true,
18981
+ includeMetadata: options.confluence?.includeMetadata ?? true,
18982
+ includeSummaryTable: options.confluence?.includeSummaryTable ?? true,
18983
+ includeErrors: options.confluence?.includeErrors ?? true,
18984
+ scenarioHeadingLevel: options.confluence?.scenarioHeadingLevel ?? 3,
18985
+ groupBy: options.confluence?.groupBy ?? "file",
18986
+ sortScenarios: options.confluence?.sortScenarios ?? "source",
18987
+ pretty: options.confluence?.pretty ?? true,
18988
+ permalinkBaseUrl: options.confluence?.permalinkBaseUrl,
18989
+ ticketUrlTemplate: options.confluence?.ticketUrlTemplate
18990
+ },
18309
18991
  astro: {
18310
18992
  assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
18311
18993
  assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
@@ -18481,6 +19163,22 @@ var ReportGenerator = class {
18481
19163
  });
18482
19164
  return formatter.format(run);
18483
19165
  }
19166
+ case "confluence": {
19167
+ const formatter = new ConfluenceFormatter({
19168
+ title: this.options.confluence.title,
19169
+ includeStatusIcons: this.options.confluence.includeStatusIcons,
19170
+ includeMetadata: this.options.confluence.includeMetadata,
19171
+ includeSummaryTable: this.options.confluence.includeSummaryTable,
19172
+ includeErrors: this.options.confluence.includeErrors,
19173
+ scenarioHeadingLevel: this.options.confluence.scenarioHeadingLevel,
19174
+ groupBy: this.options.confluence.groupBy,
19175
+ sortScenarios: this.options.confluence.sortScenarios,
19176
+ pretty: this.options.confluence.pretty,
19177
+ permalinkBaseUrl: this.options.confluence.permalinkBaseUrl,
19178
+ ticketUrlTemplate: this.options.confluence.ticketUrlTemplate
19179
+ });
19180
+ return formatter.format(run);
19181
+ }
18484
19182
  case "markdown": {
18485
19183
  const formatter = new MarkdownFormatter({
18486
19184
  title: this.options.markdown.title,
@@ -18539,6 +19237,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
18539
19237
  }
18540
19238
  export {
18541
19239
  AstroFormatter,
19240
+ ConfluenceFormatter,
18542
19241
  CucumberHtmlFormatter,
18543
19242
  CucumberJsonFormatter,
18544
19243
  CucumberMessagesFormatter,
@@ -18570,7 +19269,7 @@ export {
18570
19269
  detectPerformanceTrend,
18571
19270
  diffRuns,
18572
19271
  findGitDir,
18573
- formatDuration2 as formatDuration,
19272
+ formatDuration3 as formatDuration,
18574
19273
  generateRunComparison,
18575
19274
  generateRunId,
18576
19275
  generateTestCaseId,
@@ -18588,6 +19287,8 @@ export {
18588
19287
  normalizeVitestResults,
18589
19288
  parseEnvelopes,
18590
19289
  parseNdjson,
19290
+ publishConfluencePage,
19291
+ publishJiraIssue,
18591
19292
  readBranchName,
18592
19293
  readGitSha,
18593
19294
  readPackageVersion,