executable-stories-formatters 0.10.0 → 0.11.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/README.md +35 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1667 -149
- package/dist/cli.js.map +1 -1
- package/dist/{index-CbWFyoTx.d.cts → index-DF16Xl5i.d.cts} +7 -0
- package/dist/{index-CbWFyoTx.d.ts → index-DF16Xl5i.d.ts} +7 -0
- package/dist/index.cjs +372 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -7
- package/dist/index.d.ts +129 -7
- package/dist/index.js +363 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/schemas/raw-run.schema.json +19 -0
- package/schemas/story-report-v1.json +17 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import "fs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as path11 from "path";
|
|
4
4
|
import * as fsPromises from "fs/promises";
|
|
5
5
|
|
|
6
6
|
// src/converters/acl/status.ts
|
|
@@ -763,6 +763,15 @@ function copyDocEntry(entry) {
|
|
|
763
763
|
phase: entry.phase,
|
|
764
764
|
...children
|
|
765
765
|
};
|
|
766
|
+
case "video":
|
|
767
|
+
return {
|
|
768
|
+
kind: "video",
|
|
769
|
+
path: entry.path,
|
|
770
|
+
...entry.caption ? { caption: entry.caption } : {},
|
|
771
|
+
...entry.poster ? { poster: entry.poster } : {},
|
|
772
|
+
phase: entry.phase,
|
|
773
|
+
...children
|
|
774
|
+
};
|
|
766
775
|
case "custom":
|
|
767
776
|
return {
|
|
768
777
|
kind: "custom",
|
|
@@ -880,23 +889,23 @@ function buildFeature(relSourceFile, group) {
|
|
|
880
889
|
function ensureUniqueFeatureIds(features) {
|
|
881
890
|
const seen = /* @__PURE__ */ new Map();
|
|
882
891
|
for (const f of features) {
|
|
883
|
-
const
|
|
884
|
-
if (
|
|
885
|
-
seen.set(f.id,
|
|
892
|
+
const count2 = seen.get(f.id) ?? 0;
|
|
893
|
+
if (count2 > 0) f.id = `${f.id}-${count2 + 1}`;
|
|
894
|
+
seen.set(f.id, count2 + 1);
|
|
886
895
|
}
|
|
887
896
|
}
|
|
888
897
|
function ensureUniqueScenarioIds(feature) {
|
|
889
898
|
const seen = /* @__PURE__ */ new Map();
|
|
890
899
|
for (const s of feature.scenarios) {
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
893
|
-
const newId = `${s.id}-${
|
|
900
|
+
const count2 = seen.get(s.id) ?? 0;
|
|
901
|
+
if (count2 > 0) {
|
|
902
|
+
const newId = `${s.id}-${count2 + 1}`;
|
|
894
903
|
for (const step of s.steps) {
|
|
895
904
|
step.id = step.id.replace(s.id, newId);
|
|
896
905
|
}
|
|
897
906
|
s.id = newId;
|
|
898
907
|
}
|
|
899
|
-
seen.set(s.id,
|
|
908
|
+
seen.set(s.id, count2 + 1);
|
|
900
909
|
}
|
|
901
910
|
}
|
|
902
911
|
function toStoryReport(run) {
|
|
@@ -13924,6 +13933,23 @@ function renderDocScreenshot(entry, deps) {
|
|
|
13924
13933
|
${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
|
|
13925
13934
|
</div>`;
|
|
13926
13935
|
}
|
|
13936
|
+
function renderDocVideo(entry, deps) {
|
|
13937
|
+
const isRemote = /^(?:https?:|data:)/i.test(entry.path);
|
|
13938
|
+
const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
13939
|
+
const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
|
|
13940
|
+
if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
|
|
13941
|
+
return `<div class="doc-video doc-video-missing">
|
|
13942
|
+
<div class="doc-video-missing-label">Video unavailable</div>
|
|
13943
|
+
<div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
|
|
13944
|
+
${captionHtml}
|
|
13945
|
+
</div>`;
|
|
13946
|
+
}
|
|
13947
|
+
const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
|
|
13948
|
+
return `<div class="doc-video">
|
|
13949
|
+
<video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
|
|
13950
|
+
${captionHtml}
|
|
13951
|
+
</div>`;
|
|
13952
|
+
}
|
|
13927
13953
|
function renderDocCustom(entry, deps) {
|
|
13928
13954
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
13929
13955
|
const data = entry.data;
|
|
@@ -13977,6 +14003,9 @@ function renderDocEntry(entry, deps) {
|
|
|
13977
14003
|
case "screenshot":
|
|
13978
14004
|
html = renderDocScreenshot(entry, deps);
|
|
13979
14005
|
break;
|
|
14006
|
+
case "video":
|
|
14007
|
+
html = renderDocVideo(entry, deps);
|
|
14008
|
+
break;
|
|
13980
14009
|
case "custom":
|
|
13981
14010
|
html = renderDocCustom(entry, deps);
|
|
13982
14011
|
break;
|
|
@@ -15303,6 +15332,19 @@ var MarkdownFormatter = class {
|
|
|
15303
15332
|
case "screenshot":
|
|
15304
15333
|
lines.push(`${indent}`);
|
|
15305
15334
|
break;
|
|
15335
|
+
case "video": {
|
|
15336
|
+
const poster = entry.poster ? ` poster="${entry.poster}"` : "";
|
|
15337
|
+
lines.push(`${indent}`);
|
|
15338
|
+
lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
|
|
15339
|
+
lines.push(`${indent} <source src="${entry.path}" />`);
|
|
15340
|
+
lines.push(`${indent}</video>`);
|
|
15341
|
+
if (entry.caption) {
|
|
15342
|
+
lines.push(`${indent}`);
|
|
15343
|
+
lines.push(`${indent}*${entry.caption}*`);
|
|
15344
|
+
}
|
|
15345
|
+
lines.push(`${indent}`);
|
|
15346
|
+
break;
|
|
15347
|
+
}
|
|
15306
15348
|
case "custom":
|
|
15307
15349
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15308
15350
|
const data = entry.data;
|
|
@@ -15395,6 +15437,88 @@ function groupBy6(items, keyFn) {
|
|
|
15395
15437
|
return map;
|
|
15396
15438
|
}
|
|
15397
15439
|
|
|
15440
|
+
// src/formatters/release-manifest.ts
|
|
15441
|
+
import { createHash as createHash2 } from "crypto";
|
|
15442
|
+
var ReleaseManifestFormatter = class {
|
|
15443
|
+
format(run) {
|
|
15444
|
+
const manifest = toReleaseManifest(run);
|
|
15445
|
+
const lines = [];
|
|
15446
|
+
lines.push("# Release Manifest");
|
|
15447
|
+
lines.push("");
|
|
15448
|
+
lines.push(`Generated: ${manifest.generatedAt}`);
|
|
15449
|
+
lines.push(`Run: ${manifest.run.startedAt} to ${manifest.run.finishedAt}`);
|
|
15450
|
+
if (manifest.run.branch) lines.push(`Branch: ${manifest.run.branch}`);
|
|
15451
|
+
if (manifest.run.gitSha) lines.push(`Commit: ${manifest.run.gitSha}`);
|
|
15452
|
+
lines.push(`Tested-together hash: \`${manifest.testedTogetherHash}\``);
|
|
15453
|
+
lines.push("");
|
|
15454
|
+
lines.push("| Scenarios | Passed | Failed | Skipped | Pending |");
|
|
15455
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
15456
|
+
lines.push(`| ${manifest.run.total} | ${manifest.run.passed} | ${manifest.run.failed} | ${manifest.run.skipped} | ${manifest.run.pending} |`);
|
|
15457
|
+
lines.push("");
|
|
15458
|
+
lines.push("## Scenarios");
|
|
15459
|
+
lines.push("");
|
|
15460
|
+
lines.push("| Status | Scenario | Source | Tags |");
|
|
15461
|
+
lines.push("| --- | --- | --- | --- |");
|
|
15462
|
+
for (const scenario of manifest.scenarios) {
|
|
15463
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15464
|
+
const tags = scenario.tags.length > 0 ? scenario.tags.map((tag) => `\`${tag}\``).join(", ") : "";
|
|
15465
|
+
lines.push(`| ${renderStatus(scenario.status)} | ${escapePipe(scenario.title)} | \`${source}\` | ${tags} |`);
|
|
15466
|
+
}
|
|
15467
|
+
return lines.join("\n");
|
|
15468
|
+
}
|
|
15469
|
+
};
|
|
15470
|
+
function toReleaseManifest(run) {
|
|
15471
|
+
const scenarios = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id)).map((tc) => ({
|
|
15472
|
+
id: tc.id,
|
|
15473
|
+
title: tc.story.scenario,
|
|
15474
|
+
status: tc.status,
|
|
15475
|
+
sourceFile: tc.sourceFile,
|
|
15476
|
+
sourceLine: tc.sourceLine,
|
|
15477
|
+
tags: tc.tags
|
|
15478
|
+
}));
|
|
15479
|
+
const fingerprint = scenarios.map((scenario) => `${scenario.id}:${scenario.status}`).join("\n");
|
|
15480
|
+
return {
|
|
15481
|
+
schemaVersion: "1.0",
|
|
15482
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15483
|
+
run: {
|
|
15484
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
15485
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
15486
|
+
gitSha: run.gitSha,
|
|
15487
|
+
branch: getBranch(run),
|
|
15488
|
+
total: run.testCases.length,
|
|
15489
|
+
passed: count(run.testCases, "passed"),
|
|
15490
|
+
failed: count(run.testCases, "failed"),
|
|
15491
|
+
skipped: count(run.testCases, "skipped"),
|
|
15492
|
+
pending: count(run.testCases, "pending")
|
|
15493
|
+
},
|
|
15494
|
+
testedTogetherHash: createHash2("sha256").update(fingerprint).digest("hex"),
|
|
15495
|
+
scenarios
|
|
15496
|
+
};
|
|
15497
|
+
}
|
|
15498
|
+
function getBranch(run) {
|
|
15499
|
+
return run.ci?.branch;
|
|
15500
|
+
}
|
|
15501
|
+
function count(testCases, status) {
|
|
15502
|
+
return testCases.filter((tc) => tc.status === status).length;
|
|
15503
|
+
}
|
|
15504
|
+
function renderStatus(status) {
|
|
15505
|
+
switch (status) {
|
|
15506
|
+
case "passed":
|
|
15507
|
+
return "passed";
|
|
15508
|
+
case "failed":
|
|
15509
|
+
return "failed";
|
|
15510
|
+
case "skipped":
|
|
15511
|
+
return "skipped";
|
|
15512
|
+
case "pending":
|
|
15513
|
+
return "pending";
|
|
15514
|
+
default:
|
|
15515
|
+
return status;
|
|
15516
|
+
}
|
|
15517
|
+
}
|
|
15518
|
+
function escapePipe(value) {
|
|
15519
|
+
return value.replace(/\|/g, "\\|");
|
|
15520
|
+
}
|
|
15521
|
+
|
|
15398
15522
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
15399
15523
|
function extractFeatureName(testCases, uri) {
|
|
15400
15524
|
for (const tc of testCases) {
|
|
@@ -15461,7 +15585,7 @@ function synthesizeFeature(uri, testCases) {
|
|
|
15461
15585
|
}
|
|
15462
15586
|
|
|
15463
15587
|
// src/utils/cucumber-messages.ts
|
|
15464
|
-
import { createHash as
|
|
15588
|
+
import { createHash as createHash3 } from "crypto";
|
|
15465
15589
|
function msToTimestamp(ms) {
|
|
15466
15590
|
const seconds = Math.floor(ms / 1e3);
|
|
15467
15591
|
const nanos = Math.round(ms % 1e3 * 1e6);
|
|
@@ -15527,7 +15651,7 @@ function statusToCucumberStatus(status) {
|
|
|
15527
15651
|
}
|
|
15528
15652
|
function deterministicId(kind, salt, ...parts) {
|
|
15529
15653
|
const input = [salt, kind, ...parts].join("::");
|
|
15530
|
-
return
|
|
15654
|
+
return createHash3("sha1").update(input).digest("hex").slice(0, 36);
|
|
15531
15655
|
}
|
|
15532
15656
|
|
|
15533
15657
|
// src/formatters/cucumber-messages/build-gherkin-document.ts
|
|
@@ -16015,8 +16139,8 @@ function extractDocAttachments(step) {
|
|
|
16015
16139
|
}
|
|
16016
16140
|
return attachments;
|
|
16017
16141
|
}
|
|
16018
|
-
function guessMediaType(
|
|
16019
|
-
const lower =
|
|
16142
|
+
function guessMediaType(path12) {
|
|
16143
|
+
const lower = path12.toLowerCase();
|
|
16020
16144
|
if (lower.endsWith(".png")) return "image/png";
|
|
16021
16145
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16022
16146
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16157,11 +16281,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16157
16281
|
for (const envelope of envelopes) {
|
|
16158
16282
|
const accepted = htmlStream.write(envelope);
|
|
16159
16283
|
if (!accepted) {
|
|
16160
|
-
await new Promise((
|
|
16284
|
+
await new Promise((resolve9) => htmlStream.once("drain", resolve9));
|
|
16161
16285
|
}
|
|
16162
16286
|
}
|
|
16163
|
-
await new Promise((
|
|
16164
|
-
collector.on("finish",
|
|
16287
|
+
await new Promise((resolve9, reject) => {
|
|
16288
|
+
collector.on("finish", resolve9);
|
|
16165
16289
|
collector.on("error", reject);
|
|
16166
16290
|
htmlStream.end();
|
|
16167
16291
|
});
|
|
@@ -16462,6 +16586,8 @@ function formatDocEntry(doc) {
|
|
|
16462
16586
|
return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
|
|
16463
16587
|
case "screenshot":
|
|
16464
16588
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16589
|
+
case "video":
|
|
16590
|
+
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16465
16591
|
case "custom":
|
|
16466
16592
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
16467
16593
|
}
|
|
@@ -16918,6 +17044,8 @@ function formatDocEntry2(doc) {
|
|
|
16918
17044
|
return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
|
|
16919
17045
|
case "screenshot":
|
|
16920
17046
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17047
|
+
case "video":
|
|
17048
|
+
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
16921
17049
|
case "custom":
|
|
16922
17050
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
16923
17051
|
}
|
|
@@ -17163,19 +17291,35 @@ function replaceAssetRef(html, original, replacement) {
|
|
|
17163
17291
|
return html;
|
|
17164
17292
|
}
|
|
17165
17293
|
|
|
17294
|
+
// src/utils/source-file.ts
|
|
17295
|
+
function cleanTestStem(fileName) {
|
|
17296
|
+
const base = fileName.split(/[\\/]/).pop() ?? fileName;
|
|
17297
|
+
const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
|
|
17298
|
+
if (stripped !== base) return stripped;
|
|
17299
|
+
return base.replace(/\.[^.]+$/, "");
|
|
17300
|
+
}
|
|
17301
|
+
function humanizeSourceFile(fileName) {
|
|
17302
|
+
return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
17303
|
+
}
|
|
17304
|
+
|
|
17166
17305
|
// src/formatters/astro.ts
|
|
17167
17306
|
var AstroFormatter = class _AstroFormatter {
|
|
17168
17307
|
markdownFormatter;
|
|
17169
17308
|
title;
|
|
17309
|
+
perFileTitle;
|
|
17170
17310
|
constructor(options = {}) {
|
|
17171
17311
|
this.title = options.markdown?.title ?? "User Stories";
|
|
17312
|
+
this.perFileTitle = options.perFileTitle ?? false;
|
|
17172
17313
|
this.markdownFormatter = new MarkdownFormatter({
|
|
17173
17314
|
...options.markdown,
|
|
17174
17315
|
title: this.title,
|
|
17175
17316
|
stepStyle: "gherkin",
|
|
17176
17317
|
includeFrontMatter: false,
|
|
17177
17318
|
includeSummaryTable: false,
|
|
17178
|
-
includeMetadata: false
|
|
17319
|
+
includeMetadata: false,
|
|
17320
|
+
// A per-file page is one file already — group by suite/describe so the
|
|
17321
|
+
// body shows clean section headings, not the redundant source path.
|
|
17322
|
+
groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
|
|
17179
17323
|
});
|
|
17180
17324
|
}
|
|
17181
17325
|
format(run) {
|
|
@@ -17185,13 +17329,31 @@ var AstroFormatter = class _AstroFormatter {
|
|
|
17185
17329
|
return `${frontmatter}
|
|
17186
17330
|
${body}`;
|
|
17187
17331
|
}
|
|
17332
|
+
/**
|
|
17333
|
+
* Title for the page. A per-file page (one source file — i.e. colocated mode)
|
|
17334
|
+
* is titled by its suite/describe name, falling back to a humanized filename,
|
|
17335
|
+
* so the docs nav reads "Convert Currency" not "User Stories" six times over.
|
|
17336
|
+
* Multi-file (aggregated) pages keep the configured title.
|
|
17337
|
+
*/
|
|
17338
|
+
deriveTitle(run) {
|
|
17339
|
+
if (!this.perFileTitle) return this.title;
|
|
17340
|
+
const sourceFiles = new Set(
|
|
17341
|
+
run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
|
|
17342
|
+
);
|
|
17343
|
+
if (sourceFiles.size !== 1) return this.title;
|
|
17344
|
+
const suites = new Set(
|
|
17345
|
+
run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
|
|
17346
|
+
);
|
|
17347
|
+
if (suites.size === 1) return [...suites][0];
|
|
17348
|
+
return humanizeSourceFile([...sourceFiles][0]) || this.title;
|
|
17349
|
+
}
|
|
17188
17350
|
buildFrontmatter(run) {
|
|
17189
17351
|
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
17190
|
-
const
|
|
17191
|
-
const description = `${
|
|
17352
|
+
const count2 = run.testCases.length;
|
|
17353
|
+
const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
17192
17354
|
const lines = [
|
|
17193
17355
|
"---",
|
|
17194
|
-
`title: ${this.
|
|
17356
|
+
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
17195
17357
|
`description: ${description}`,
|
|
17196
17358
|
"sidebar:",
|
|
17197
17359
|
" badge:",
|
|
@@ -17209,6 +17371,12 @@ ${body}`;
|
|
|
17209
17371
|
return { text: "Passed", variant: "success" };
|
|
17210
17372
|
}
|
|
17211
17373
|
};
|
|
17374
|
+
function yamlScalar(value) {
|
|
17375
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
17376
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
17377
|
+
}
|
|
17378
|
+
return value;
|
|
17379
|
+
}
|
|
17212
17380
|
|
|
17213
17381
|
// src/formatters/confluence.ts
|
|
17214
17382
|
var ConfluenceFormatter = class {
|
|
@@ -17485,6 +17653,15 @@ ${tc.errorStack}` : "");
|
|
|
17485
17653
|
])
|
|
17486
17654
|
);
|
|
17487
17655
|
break;
|
|
17656
|
+
case "video":
|
|
17657
|
+
content.push(
|
|
17658
|
+
paragraph([
|
|
17659
|
+
text(entry.caption ?? "Video", strong()),
|
|
17660
|
+
text(": "),
|
|
17661
|
+
link(entry.path, entry.path)
|
|
17662
|
+
])
|
|
17663
|
+
);
|
|
17664
|
+
break;
|
|
17488
17665
|
case "custom":
|
|
17489
17666
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
17490
17667
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -17654,6 +17831,13 @@ function scanMarkdownAssets(markdown) {
|
|
|
17654
17831
|
found.add(src);
|
|
17655
17832
|
}
|
|
17656
17833
|
}
|
|
17834
|
+
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
17835
|
+
while ((match = posterRe.exec(stripped)) !== null) {
|
|
17836
|
+
const src = match[1].trim();
|
|
17837
|
+
if (isLocalPath(src)) {
|
|
17838
|
+
found.add(src);
|
|
17839
|
+
}
|
|
17840
|
+
}
|
|
17657
17841
|
return Array.from(found);
|
|
17658
17842
|
}
|
|
17659
17843
|
function splitByCode(markdown) {
|
|
@@ -17704,6 +17888,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
|
17704
17888
|
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
17705
17889
|
}
|
|
17706
17890
|
);
|
|
17891
|
+
result = result.replace(
|
|
17892
|
+
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
17893
|
+
(full, pre, src, post) => {
|
|
17894
|
+
const trimmed = src.trim();
|
|
17895
|
+
if (!isLocalPath(trimmed)) return full;
|
|
17896
|
+
if (pathMap) {
|
|
17897
|
+
const mapped = pathMap.get(trimmed);
|
|
17898
|
+
if (mapped === void 0) return full;
|
|
17899
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
17900
|
+
}
|
|
17901
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
17902
|
+
}
|
|
17903
|
+
);
|
|
17707
17904
|
return result;
|
|
17708
17905
|
}
|
|
17709
17906
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
@@ -18151,14 +18348,14 @@ ${result.errors.join("\n")}`);
|
|
|
18151
18348
|
}
|
|
18152
18349
|
|
|
18153
18350
|
// src/coverage-index.ts
|
|
18154
|
-
function normalizePath(
|
|
18155
|
-
return
|
|
18351
|
+
function normalizePath(path12) {
|
|
18352
|
+
return path12.replace(/^\.\//, "");
|
|
18156
18353
|
}
|
|
18157
18354
|
function scenariosCoveringPaths(index, paths) {
|
|
18158
18355
|
const queries = paths.map(normalizePath);
|
|
18159
18356
|
return index.scenarios.filter(
|
|
18160
18357
|
(scenario) => scenario.covers.some(
|
|
18161
|
-
(glob) => queries.some((
|
|
18358
|
+
(glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
|
|
18162
18359
|
)
|
|
18163
18360
|
);
|
|
18164
18361
|
}
|
|
@@ -20075,18 +20272,18 @@ function deriveChangeType(tags) {
|
|
|
20075
20272
|
}
|
|
20076
20273
|
return "unknown";
|
|
20077
20274
|
}
|
|
20078
|
-
function extensionOf(
|
|
20079
|
-
const base =
|
|
20275
|
+
function extensionOf(path12) {
|
|
20276
|
+
const base = path12.split("/").pop() ?? path12;
|
|
20080
20277
|
const dot = base.lastIndexOf(".");
|
|
20081
20278
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20082
20279
|
}
|
|
20083
|
-
function isTestFile(
|
|
20084
|
-
return TEST_INFIX.test(
|
|
20280
|
+
function isTestFile(path12) {
|
|
20281
|
+
return TEST_INFIX.test(path12);
|
|
20085
20282
|
}
|
|
20086
|
-
function isReviewableSource(
|
|
20087
|
-
if (isTestFile(
|
|
20088
|
-
if (
|
|
20089
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
20283
|
+
function isReviewableSource(path12) {
|
|
20284
|
+
if (isTestFile(path12)) return false;
|
|
20285
|
+
if (path12.endsWith(".d.ts")) return false;
|
|
20286
|
+
return CODE_EXTENSIONS.has(extensionOf(path12));
|
|
20090
20287
|
}
|
|
20091
20288
|
function testBaseKey(testFile) {
|
|
20092
20289
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -20190,7 +20387,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
20190
20387
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20191
20388
|
const key = testBaseKey(testCase.sourceFile);
|
|
20192
20389
|
const coversFiles = changedSourcePaths.filter(
|
|
20193
|
-
(
|
|
20390
|
+
(path12) => sourceBaseKey(path12) === key
|
|
20194
20391
|
);
|
|
20195
20392
|
return {
|
|
20196
20393
|
id: testCase.id,
|
|
@@ -20720,11 +20917,116 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
20720
20917
|
}
|
|
20721
20918
|
};
|
|
20722
20919
|
|
|
20920
|
+
// src/deploy/ledger.ts
|
|
20921
|
+
import * as fs9 from "fs";
|
|
20922
|
+
import * as path10 from "path";
|
|
20923
|
+
function createEmptyLedger() {
|
|
20924
|
+
return {
|
|
20925
|
+
deployments: [],
|
|
20926
|
+
schemaVersion: 1
|
|
20927
|
+
};
|
|
20928
|
+
}
|
|
20929
|
+
function loadLedger(ledgerPath) {
|
|
20930
|
+
const resolved = path10.resolve(ledgerPath);
|
|
20931
|
+
if (!fs9.existsSync(resolved)) {
|
|
20932
|
+
return createEmptyLedger();
|
|
20933
|
+
}
|
|
20934
|
+
try {
|
|
20935
|
+
const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
|
|
20936
|
+
if (raw.schemaVersion !== 1) {
|
|
20937
|
+
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
20938
|
+
}
|
|
20939
|
+
return raw;
|
|
20940
|
+
} catch (err) {
|
|
20941
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20942
|
+
throw new Error(`Failed to load deployment ledger at ${resolved}: ${msg}`);
|
|
20943
|
+
}
|
|
20944
|
+
}
|
|
20945
|
+
function saveLedger(ledger, ledgerPath) {
|
|
20946
|
+
const resolved = path10.resolve(ledgerPath);
|
|
20947
|
+
const dir = path10.dirname(resolved);
|
|
20948
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
20949
|
+
fs9.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
20950
|
+
}
|
|
20951
|
+
function getLatestDeployment(ledger, environment) {
|
|
20952
|
+
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
20953
|
+
}
|
|
20954
|
+
|
|
20955
|
+
// src/deploy/index.ts
|
|
20956
|
+
function recordDeployment(args) {
|
|
20957
|
+
const ledger = loadLedger(args.ledgerPath);
|
|
20958
|
+
const previous = getLatestDeployment(ledger, args.environment);
|
|
20959
|
+
const entry = {
|
|
20960
|
+
environment: args.environment,
|
|
20961
|
+
tag: args.tag,
|
|
20962
|
+
sha: args.run.gitSha,
|
|
20963
|
+
runFile: args.runFilePath,
|
|
20964
|
+
scenarioIds: args.run.testCases.map((tc) => tc.id),
|
|
20965
|
+
scenarioStatuses: Object.fromEntries(args.run.testCases.map((tc) => [tc.id, tc.status])),
|
|
20966
|
+
timestamp: new Date(args.run.finishedAtMs).toISOString(),
|
|
20967
|
+
summary: countStatuses(args.run)
|
|
20968
|
+
};
|
|
20969
|
+
ledger.deployments.push(entry);
|
|
20970
|
+
if (previous) {
|
|
20971
|
+
const previousIds = new Set(previous.scenarioIds);
|
|
20972
|
+
const added = entry.scenarioIds.filter((id) => !previousIds.has(id)).length;
|
|
20973
|
+
const removed = previous.scenarioIds.filter((id) => !entry.scenarioIds.includes(id)).length;
|
|
20974
|
+
if (added > 0 || removed > 0) {
|
|
20975
|
+
}
|
|
20976
|
+
}
|
|
20977
|
+
saveLedger(ledger, args.ledgerPath);
|
|
20978
|
+
return { entry, ledgerPath: args.ledgerPath };
|
|
20979
|
+
}
|
|
20980
|
+
function getDeploymentStatus(ledgerPath) {
|
|
20981
|
+
const ledger = loadLedger(ledgerPath);
|
|
20982
|
+
const environments = {};
|
|
20983
|
+
for (const entry of ledger.deployments) {
|
|
20984
|
+
environments[entry.environment] = {
|
|
20985
|
+
latest: entry,
|
|
20986
|
+
previousDeployment: environments[entry.environment]?.latest
|
|
20987
|
+
};
|
|
20988
|
+
}
|
|
20989
|
+
return { environments, ledgerPath };
|
|
20990
|
+
}
|
|
20991
|
+
function getEnvironmentDrift(ledgerPath, envA, envB) {
|
|
20992
|
+
const ledger = loadLedger(ledgerPath);
|
|
20993
|
+
const aEntry = getLatestDeployment(ledger, envA);
|
|
20994
|
+
const bEntry = getLatestDeployment(ledger, envB);
|
|
20995
|
+
if (!aEntry) {
|
|
20996
|
+
throw new Error(`No deployment found for environment "${envA}"`);
|
|
20997
|
+
}
|
|
20998
|
+
if (!bEntry) {
|
|
20999
|
+
throw new Error(`No deployment found for environment "${envB}"`);
|
|
21000
|
+
}
|
|
21001
|
+
const aIds = new Set(aEntry.scenarioIds);
|
|
21002
|
+
const bIds = new Set(bEntry.scenarioIds);
|
|
21003
|
+
const onlyInA = aEntry.scenarioIds.filter((id) => !bIds.has(id));
|
|
21004
|
+
const onlyInB = bEntry.scenarioIds.filter((id) => !aIds.has(id));
|
|
21005
|
+
const inBoth = aEntry.scenarioIds.filter((id) => bIds.has(id));
|
|
21006
|
+
const statusChanged = inBoth.map((id) => ({
|
|
21007
|
+
id,
|
|
21008
|
+
statusA: aEntry.scenarioStatuses?.[id] ?? "unknown",
|
|
21009
|
+
statusB: bEntry.scenarioStatuses?.[id] ?? "unknown"
|
|
21010
|
+
})).filter((item) => item.statusA !== item.statusB);
|
|
21011
|
+
return { environmentA: envA, environmentB: envB, onlyInA, onlyInB, inBoth, statusChanged, aEntry, bEntry };
|
|
21012
|
+
}
|
|
21013
|
+
function countStatuses(run) {
|
|
21014
|
+
const summary = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
|
|
21015
|
+
for (const tc of run.testCases) {
|
|
21016
|
+
summary.total++;
|
|
21017
|
+
if (tc.status in summary) {
|
|
21018
|
+
summary[tc.status]++;
|
|
21019
|
+
}
|
|
21020
|
+
}
|
|
21021
|
+
return summary;
|
|
21022
|
+
}
|
|
21023
|
+
|
|
20723
21024
|
// src/index.ts
|
|
20724
21025
|
var FORMAT_EXTENSIONS = {
|
|
20725
21026
|
astro: ".md",
|
|
20726
21027
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
20727
21028
|
markdown: ".md",
|
|
21029
|
+
"release-manifest": ".release-manifest.md",
|
|
20728
21030
|
html: ".html",
|
|
20729
21031
|
"cucumber-html": ".cucumber.html",
|
|
20730
21032
|
junit: ".junit.xml",
|
|
@@ -20734,6 +21036,10 @@ var FORMAT_EXTENSIONS = {
|
|
|
20734
21036
|
"scenario-index-json": ".scenarios-index.json",
|
|
20735
21037
|
"story-report-json": ".story-report.json"
|
|
20736
21038
|
};
|
|
21039
|
+
function joinNameAndExt(name, ext) {
|
|
21040
|
+
const stutter = `.${name}.`;
|
|
21041
|
+
return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
|
|
21042
|
+
}
|
|
20737
21043
|
var TEST_EXTENSIONS = [
|
|
20738
21044
|
".test.ts",
|
|
20739
21045
|
".test.tsx",
|
|
@@ -20759,11 +21065,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20759
21065
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20760
21066
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
20761
21067
|
if (mode === "aggregated") {
|
|
20762
|
-
return toPosix(
|
|
21068
|
+
return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
20763
21069
|
}
|
|
20764
21070
|
const normalizedSource = toPosix(sourceFile);
|
|
20765
|
-
const dirOfSource =
|
|
20766
|
-
let baseName =
|
|
21071
|
+
const dirOfSource = path11.posix.dirname(normalizedSource);
|
|
21072
|
+
let baseName = path11.posix.basename(normalizedSource);
|
|
20767
21073
|
for (const testExt of TEST_EXTENSIONS) {
|
|
20768
21074
|
if (baseName.endsWith(testExt)) {
|
|
20769
21075
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -20772,9 +21078,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20772
21078
|
}
|
|
20773
21079
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
20774
21080
|
if (colocatedStyle === "adjacent") {
|
|
20775
|
-
return toPosix(
|
|
21081
|
+
return toPosix(path11.posix.join(dirOfSource, fileName));
|
|
20776
21082
|
}
|
|
20777
|
-
|
|
21083
|
+
if (colocatedStyle === "flat") {
|
|
21084
|
+
return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
21085
|
+
}
|
|
21086
|
+
return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
20778
21087
|
}
|
|
20779
21088
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
20780
21089
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -20981,8 +21290,8 @@ var ReportGenerator = class {
|
|
|
20981
21290
|
if (astroPaths) {
|
|
20982
21291
|
for (const mdPath of astroPaths) {
|
|
20983
21292
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
20984
|
-
const mdDir =
|
|
20985
|
-
const assetsDir =
|
|
21293
|
+
const mdDir = path11.dirname(mdPath);
|
|
21294
|
+
const assetsDir = path11.resolve(this.options.astro.assetsDir);
|
|
20986
21295
|
const result = copyMarkdownAssets({
|
|
20987
21296
|
markdown: content,
|
|
20988
21297
|
markdownDir: mdDir,
|
|
@@ -21013,9 +21322,9 @@ var ReportGenerator = class {
|
|
|
21013
21322
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
21014
21323
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21015
21324
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
21016
|
-
const outputPath = toPosix(
|
|
21325
|
+
const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
21017
21326
|
const content = await this.formatContent(run, format);
|
|
21018
|
-
const dir =
|
|
21327
|
+
const dir = path11.dirname(outputPath);
|
|
21019
21328
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21020
21329
|
await this.deps.writeFile(outputPath, content);
|
|
21021
21330
|
return [outputPath];
|
|
@@ -21027,7 +21336,7 @@ var ReportGenerator = class {
|
|
|
21027
21336
|
testCases
|
|
21028
21337
|
};
|
|
21029
21338
|
const content = await this.formatContent(groupRun, format);
|
|
21030
|
-
const dir =
|
|
21339
|
+
const dir = path11.dirname(outputPath);
|
|
21031
21340
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
21032
21341
|
await this.deps.writeFile(outputPath, content);
|
|
21033
21342
|
writtenPaths.push(outputPath);
|
|
@@ -21093,6 +21402,8 @@ var ReportGenerator = class {
|
|
|
21093
21402
|
case "astro": {
|
|
21094
21403
|
const formatter = new AstroFormatter({
|
|
21095
21404
|
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
21405
|
+
// Colocated = one page per file, so title each by its own suite/file.
|
|
21406
|
+
perFileTitle: this.options.output.mode === "colocated",
|
|
21096
21407
|
markdown: this.options.astro.markdown
|
|
21097
21408
|
});
|
|
21098
21409
|
return formatter.format(run);
|
|
@@ -21134,6 +21445,10 @@ var ReportGenerator = class {
|
|
|
21134
21445
|
});
|
|
21135
21446
|
return formatter.format(run);
|
|
21136
21447
|
}
|
|
21448
|
+
case "release-manifest": {
|
|
21449
|
+
const formatter = new ReleaseManifestFormatter();
|
|
21450
|
+
return formatter.format(run);
|
|
21451
|
+
}
|
|
21137
21452
|
case "story-report-json": {
|
|
21138
21453
|
const formatter = new StoryReportJsonFormatter({
|
|
21139
21454
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21168,7 +21483,7 @@ async function generateRunComparison(args) {
|
|
|
21168
21483
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
21169
21484
|
for (const format of args.formats) {
|
|
21170
21485
|
const ext = format === "html" ? ".html" : ".md";
|
|
21171
|
-
const outputPath = toPosix(
|
|
21486
|
+
const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
|
|
21172
21487
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
21173
21488
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
21174
21489
|
files.push(outputPath);
|
|
@@ -21202,6 +21517,7 @@ export {
|
|
|
21202
21517
|
MIN_METRIC_SAMPLES,
|
|
21203
21518
|
MIN_PERF_SAMPLES,
|
|
21204
21519
|
MarkdownFormatter,
|
|
21520
|
+
ReleaseManifestFormatter,
|
|
21205
21521
|
ReportGenerator,
|
|
21206
21522
|
ReviewHtmlFormatter,
|
|
21207
21523
|
ReviewMarkdownFormatter,
|
|
@@ -21241,10 +21557,13 @@ export {
|
|
|
21241
21557
|
generateTestCaseId,
|
|
21242
21558
|
getAvailableThemes,
|
|
21243
21559
|
getCssOnlyThemes,
|
|
21560
|
+
getDeploymentStatus,
|
|
21561
|
+
getEnvironmentDrift,
|
|
21244
21562
|
gradeEvidence,
|
|
21245
21563
|
hasSufficientHistory,
|
|
21246
21564
|
isReviewableSource,
|
|
21247
21565
|
isTestFile,
|
|
21566
|
+
joinNameAndExt,
|
|
21248
21567
|
listScenarios,
|
|
21249
21568
|
loadHistory,
|
|
21250
21569
|
mergeStepResults,
|
|
@@ -21261,6 +21580,7 @@ export {
|
|
|
21261
21580
|
readBranchName,
|
|
21262
21581
|
readGitSha,
|
|
21263
21582
|
readPackageVersion,
|
|
21583
|
+
recordDeployment,
|
|
21264
21584
|
regenerateArtifacts,
|
|
21265
21585
|
resolveAttachment,
|
|
21266
21586
|
resolveAttachments,
|
|
@@ -21280,6 +21600,7 @@ export {
|
|
|
21280
21600
|
toBehaviorManifest,
|
|
21281
21601
|
toCIInfo,
|
|
21282
21602
|
toRawCIInfo,
|
|
21603
|
+
toReleaseManifest,
|
|
21283
21604
|
toScenarioIndex,
|
|
21284
21605
|
toStoryReport,
|
|
21285
21606
|
tryGetActiveOtelContext,
|