executable-stories-formatters 0.12.0 → 0.14.0
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 +20 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +991 -111
- package/dist/cli.js.map +1 -1
- package/dist/{index-mrT6-JSt.d.cts → index-CXrzCk9p.d.cts} +1 -1
- package/dist/{index-mrT6-JSt.d.ts → index-CXrzCk9p.d.ts} +1 -1
- package/dist/index.cjs +466 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -4
- package/dist/index.d.ts +252 -4
- package/dist/index.js +458 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/raw-run.schema.json +9 -0
package/dist/index.cjs
CHANGED
|
@@ -55,12 +55,16 @@ __export(src_exports, {
|
|
|
55
55
|
STORY_REPORT_SCHEMA_VERSION: () => STORY_REPORT_SCHEMA_VERSION,
|
|
56
56
|
ScenarioIndexJsonFormatter: () => ScenarioIndexJsonFormatter,
|
|
57
57
|
StoryReportJsonFormatter: () => StoryReportJsonFormatter,
|
|
58
|
+
TraceabilityMatrixFormatter: () => TraceabilityMatrixFormatter,
|
|
58
59
|
adaptJestRun: () => adaptJestRun,
|
|
59
60
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
60
61
|
adaptVitestRun: () => adaptVitestRun,
|
|
61
62
|
assertValidRun: () => assertValidRun,
|
|
63
|
+
buildCheck: () => buildCheck,
|
|
64
|
+
buildGoal: () => buildGoal,
|
|
62
65
|
buildHtmlDocEntry: () => buildHtmlDocEntry,
|
|
63
66
|
buildReview: () => buildReview,
|
|
67
|
+
buildTriage: () => buildTriage,
|
|
64
68
|
bundleAssets: () => bundleAssets,
|
|
65
69
|
calculateFlakiness: () => calculateFlakiness,
|
|
66
70
|
calculateStability: () => calculateStability,
|
|
@@ -110,6 +114,9 @@ __export(src_exports, {
|
|
|
110
114
|
readPackageVersion: () => readPackageVersion,
|
|
111
115
|
recordDeployment: () => recordDeployment,
|
|
112
116
|
regenerateArtifacts: () => regenerateArtifacts,
|
|
117
|
+
renderCheck: () => renderCheck,
|
|
118
|
+
renderGoal: () => renderGoal,
|
|
119
|
+
renderTriage: () => renderTriage,
|
|
113
120
|
resolveAttachment: () => resolveAttachment,
|
|
114
121
|
resolveAttachments: () => resolveAttachments,
|
|
115
122
|
resolveTheme: () => resolveTheme,
|
|
@@ -131,6 +138,7 @@ __export(src_exports, {
|
|
|
131
138
|
toReleaseManifest: () => toReleaseManifest,
|
|
132
139
|
toScenarioIndex: () => toScenarioIndex,
|
|
133
140
|
toStoryReport: () => toStoryReport,
|
|
141
|
+
toTraceabilityMatrix: () => toTraceabilityMatrix,
|
|
134
142
|
tryGetActiveOtelContext: () => tryGetActiveOtelContext,
|
|
135
143
|
updateHistory: () => updateHistory,
|
|
136
144
|
validateCanonicalRun: () => validateCanonicalRun
|
|
@@ -663,14 +671,14 @@ var CucumberJsonFormatter = class {
|
|
|
663
671
|
duration: 0
|
|
664
672
|
};
|
|
665
673
|
}
|
|
666
|
-
const
|
|
674
|
+
const statusMap2 = {
|
|
667
675
|
passed: "passed",
|
|
668
676
|
failed: "failed",
|
|
669
677
|
skipped: "skipped",
|
|
670
678
|
pending: "pending"
|
|
671
679
|
};
|
|
672
680
|
const stepResult = {
|
|
673
|
-
status:
|
|
681
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
674
682
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
675
683
|
duration: result.durationMs * 1e6
|
|
676
684
|
};
|
|
@@ -15261,7 +15269,9 @@ var MarkdownFormatter = class {
|
|
|
15261
15269
|
ticketUrlTemplate: options.ticketUrlTemplate,
|
|
15262
15270
|
traceUrlTemplate: options.traceUrlTemplate,
|
|
15263
15271
|
includeSourceLinks: options.includeSourceLinks ?? true,
|
|
15264
|
-
customRenderers: options.customRenderers
|
|
15272
|
+
customRenderers: options.customRenderers,
|
|
15273
|
+
scenarioAnchor: options.scenarioAnchor,
|
|
15274
|
+
scenarioBadge: options.scenarioBadge
|
|
15265
15275
|
};
|
|
15266
15276
|
}
|
|
15267
15277
|
/**
|
|
@@ -15448,6 +15458,11 @@ var MarkdownFormatter = class {
|
|
|
15448
15458
|
* Render a single scenario.
|
|
15449
15459
|
*/
|
|
15450
15460
|
renderScenario(lines, tc) {
|
|
15461
|
+
const anchorId = this.options.scenarioAnchor?.(tc);
|
|
15462
|
+
if (anchorId) {
|
|
15463
|
+
lines.push(`<a id="${anchorId}"></a>`);
|
|
15464
|
+
lines.push("");
|
|
15465
|
+
}
|
|
15451
15466
|
if (this.options.customRenderers?.renderScenarioHeader) {
|
|
15452
15467
|
const custom = this.options.customRenderers.renderScenarioHeader(tc);
|
|
15453
15468
|
if (custom !== null) {
|
|
@@ -15463,6 +15478,10 @@ var MarkdownFormatter = class {
|
|
|
15463
15478
|
icon = this.getStatusIcon(tc.status) + " ";
|
|
15464
15479
|
}
|
|
15465
15480
|
lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
|
|
15481
|
+
const badge = this.options.scenarioBadge?.(tc);
|
|
15482
|
+
if (badge) {
|
|
15483
|
+
lines.push(badge);
|
|
15484
|
+
}
|
|
15466
15485
|
if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
|
|
15467
15486
|
const permalink = this.buildPermalink(tc);
|
|
15468
15487
|
lines.push(`Source: [${tc.sourceFile}](${permalink})`);
|
|
@@ -15854,6 +15873,132 @@ function escapePipe(value) {
|
|
|
15854
15873
|
return value.replace(/\|/g, "\\|");
|
|
15855
15874
|
}
|
|
15856
15875
|
|
|
15876
|
+
// src/formatters/traceability-matrix.ts
|
|
15877
|
+
var TraceabilityMatrixFormatter = class {
|
|
15878
|
+
format(run) {
|
|
15879
|
+
const matrix = toTraceabilityMatrix(run);
|
|
15880
|
+
const lines = [];
|
|
15881
|
+
lines.push("# Traceability Matrix");
|
|
15882
|
+
lines.push("");
|
|
15883
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
15884
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
15885
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
15886
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
15887
|
+
lines.push("");
|
|
15888
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
15889
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
15890
|
+
lines.push(
|
|
15891
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
15892
|
+
);
|
|
15893
|
+
lines.push("");
|
|
15894
|
+
for (const req of matrix.requirements) {
|
|
15895
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
15896
|
+
lines.push(`## ${heading2}`);
|
|
15897
|
+
lines.push("");
|
|
15898
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
15899
|
+
if (req.covers.length > 0) {
|
|
15900
|
+
lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
|
|
15901
|
+
}
|
|
15902
|
+
lines.push("");
|
|
15903
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
15904
|
+
lines.push("| --- | --- | --- | --- |");
|
|
15905
|
+
for (const scenario of req.scenarios) {
|
|
15906
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15907
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
|
|
15908
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
15909
|
+
}
|
|
15910
|
+
lines.push("");
|
|
15911
|
+
}
|
|
15912
|
+
if (matrix.untraced.length > 0) {
|
|
15913
|
+
lines.push("## Untraced scenarios");
|
|
15914
|
+
lines.push("");
|
|
15915
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
15916
|
+
lines.push("");
|
|
15917
|
+
lines.push("| Status | Scenario | Source |");
|
|
15918
|
+
lines.push("| --- | --- | --- |");
|
|
15919
|
+
for (const scenario of matrix.untraced) {
|
|
15920
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15921
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
15922
|
+
}
|
|
15923
|
+
lines.push("");
|
|
15924
|
+
}
|
|
15925
|
+
return lines.join("\n").trimEnd();
|
|
15926
|
+
}
|
|
15927
|
+
};
|
|
15928
|
+
function toTraceabilityMatrix(run) {
|
|
15929
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
15930
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
15931
|
+
const untraced = [];
|
|
15932
|
+
for (const tc of sorted) {
|
|
15933
|
+
const tickets = tc.story.tickets ?? [];
|
|
15934
|
+
if (tickets.length === 0) {
|
|
15935
|
+
untraced.push({
|
|
15936
|
+
id: tc.id,
|
|
15937
|
+
title: tc.story.scenario,
|
|
15938
|
+
status: tc.status,
|
|
15939
|
+
sourceFile: tc.sourceFile,
|
|
15940
|
+
sourceLine: tc.sourceLine
|
|
15941
|
+
});
|
|
15942
|
+
continue;
|
|
15943
|
+
}
|
|
15944
|
+
for (const ticket of tickets) {
|
|
15945
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
15946
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
15947
|
+
entry.cases.push(tc);
|
|
15948
|
+
byTicket.set(ticket.id, entry);
|
|
15949
|
+
}
|
|
15950
|
+
}
|
|
15951
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
15952
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
15953
|
+
id: tc.id,
|
|
15954
|
+
title: tc.story.scenario,
|
|
15955
|
+
status: tc.status,
|
|
15956
|
+
sourceFile: tc.sourceFile,
|
|
15957
|
+
sourceLine: tc.sourceLine,
|
|
15958
|
+
covers: tc.story.covers ?? []
|
|
15959
|
+
}));
|
|
15960
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
15961
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
15962
|
+
});
|
|
15963
|
+
return {
|
|
15964
|
+
schemaVersion: "1.0",
|
|
15965
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15966
|
+
run: {
|
|
15967
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
15968
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
15969
|
+
gitSha: run.gitSha,
|
|
15970
|
+
branch: run.ci?.branch
|
|
15971
|
+
},
|
|
15972
|
+
summary: {
|
|
15973
|
+
requirements: requirements.length,
|
|
15974
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
15975
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
15976
|
+
scenarios: run.testCases.length,
|
|
15977
|
+
untracedScenarios: untraced.length
|
|
15978
|
+
},
|
|
15979
|
+
requirements,
|
|
15980
|
+
untraced
|
|
15981
|
+
};
|
|
15982
|
+
}
|
|
15983
|
+
function requirementStatus(cases) {
|
|
15984
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
15985
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
15986
|
+
return "incomplete";
|
|
15987
|
+
}
|
|
15988
|
+
function renderRequirementStatus(status) {
|
|
15989
|
+
switch (status) {
|
|
15990
|
+
case "verified":
|
|
15991
|
+
return "verified (all scenarios passed)";
|
|
15992
|
+
case "failing":
|
|
15993
|
+
return "failing (a scenario failed)";
|
|
15994
|
+
default:
|
|
15995
|
+
return "incomplete (no scenario passed yet)";
|
|
15996
|
+
}
|
|
15997
|
+
}
|
|
15998
|
+
function escapePipe2(value) {
|
|
15999
|
+
return value.replace(/\|/g, "\\|");
|
|
16000
|
+
}
|
|
16001
|
+
|
|
15857
16002
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
15858
16003
|
function extractFeatureName(testCases, uri) {
|
|
15859
16004
|
for (const tc of testCases) {
|
|
@@ -18153,12 +18298,16 @@ function groupBy7(items, keyFn) {
|
|
|
18153
18298
|
var fs5 = __toESM(require("fs"), 1);
|
|
18154
18299
|
var path6 = __toESM(require("path"), 1);
|
|
18155
18300
|
var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
|
|
18156
|
-
function
|
|
18301
|
+
function isRemoteRef(src) {
|
|
18157
18302
|
const trimmed = src.trim();
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
|
|
18303
|
+
return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
18304
|
+
}
|
|
18305
|
+
function isAbsoluteRef(src) {
|
|
18306
|
+
const trimmed = src.trim();
|
|
18307
|
+
return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
|
|
18308
|
+
}
|
|
18309
|
+
function isRelativeLocalPath(src) {
|
|
18310
|
+
return !isRemoteRef(src) && !isAbsoluteRef(src);
|
|
18162
18311
|
}
|
|
18163
18312
|
function stripCodeContent(markdown) {
|
|
18164
18313
|
let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
@@ -18174,21 +18323,21 @@ function scanMarkdownAssets(markdown) {
|
|
|
18174
18323
|
let match;
|
|
18175
18324
|
while ((match = mdImageRe.exec(stripped)) !== null) {
|
|
18176
18325
|
const src = match[1].trim();
|
|
18177
|
-
if (
|
|
18326
|
+
if (!isRemoteRef(src)) {
|
|
18178
18327
|
found.add(src);
|
|
18179
18328
|
}
|
|
18180
18329
|
}
|
|
18181
18330
|
const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
|
|
18182
18331
|
while ((match = htmlSrcRe.exec(stripped)) !== null) {
|
|
18183
18332
|
const src = match[1].trim();
|
|
18184
|
-
if (
|
|
18333
|
+
if (!isRemoteRef(src)) {
|
|
18185
18334
|
found.add(src);
|
|
18186
18335
|
}
|
|
18187
18336
|
}
|
|
18188
18337
|
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
18189
18338
|
while ((match = posterRe.exec(stripped)) !== null) {
|
|
18190
18339
|
const src = match[1].trim();
|
|
18191
|
-
if (
|
|
18340
|
+
if (!isRemoteRef(src)) {
|
|
18192
18341
|
found.add(src);
|
|
18193
18342
|
}
|
|
18194
18343
|
}
|
|
@@ -18214,48 +18363,21 @@ function isCode(segment) {
|
|
|
18214
18363
|
const trimmed = segment.trimStart();
|
|
18215
18364
|
return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
|
|
18216
18365
|
}
|
|
18366
|
+
function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
|
|
18367
|
+
if (isRemoteRef(trimmed)) return null;
|
|
18368
|
+
if (pathMap) {
|
|
18369
|
+
const mapped = pathMap.get(trimmed);
|
|
18370
|
+
return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
|
|
18371
|
+
}
|
|
18372
|
+
if (!isRelativeLocalPath(trimmed)) return null;
|
|
18373
|
+
return `${assetsBaseUrl}/${trimmed}`;
|
|
18374
|
+
}
|
|
18217
18375
|
function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18224
|
-
if (pathMap) {
|
|
18225
|
-
const mapped = pathMap.get(trimmed);
|
|
18226
|
-
if (mapped === void 0) return full;
|
|
18227
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18228
|
-
}
|
|
18229
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18230
|
-
}
|
|
18231
|
-
);
|
|
18232
|
-
result = result.replace(
|
|
18233
|
-
/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18234
|
-
(full, pre, src, post) => {
|
|
18235
|
-
const trimmed = src.trim();
|
|
18236
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18237
|
-
if (pathMap) {
|
|
18238
|
-
const mapped = pathMap.get(trimmed);
|
|
18239
|
-
if (mapped === void 0) return full;
|
|
18240
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18241
|
-
}
|
|
18242
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18243
|
-
}
|
|
18244
|
-
);
|
|
18245
|
-
result = result.replace(
|
|
18246
|
-
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18247
|
-
(full, pre, src, post) => {
|
|
18248
|
-
const trimmed = src.trim();
|
|
18249
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18250
|
-
if (pathMap) {
|
|
18251
|
-
const mapped = pathMap.get(trimmed);
|
|
18252
|
-
if (mapped === void 0) return full;
|
|
18253
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18254
|
-
}
|
|
18255
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18256
|
-
}
|
|
18257
|
-
);
|
|
18258
|
-
return result;
|
|
18376
|
+
const rewrite = (full, pre, src, post) => {
|
|
18377
|
+
const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
|
|
18378
|
+
return target === null ? full : `${pre}${target}${post}`;
|
|
18379
|
+
};
|
|
18380
|
+
return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
|
|
18259
18381
|
}
|
|
18260
18382
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
18261
18383
|
return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
|
|
@@ -18272,8 +18394,9 @@ function copyMarkdownAssets(options) {
|
|
|
18272
18394
|
const pathMap = /* @__PURE__ */ new Map();
|
|
18273
18395
|
const missing = [];
|
|
18274
18396
|
for (const ref of refs) {
|
|
18275
|
-
const absPath = path6.resolve(markdownDir, ref);
|
|
18397
|
+
const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
|
|
18276
18398
|
if (!fs5.existsSync(absPath)) {
|
|
18399
|
+
if (isAbsoluteRef(ref)) continue;
|
|
18277
18400
|
if (!allowMissing) {
|
|
18278
18401
|
throw new Error(`Asset not found: ${absPath}`);
|
|
18279
18402
|
}
|
|
@@ -20602,6 +20725,280 @@ function collectDocKinds(testCase) {
|
|
|
20602
20725
|
return [...kinds].sort();
|
|
20603
20726
|
}
|
|
20604
20727
|
|
|
20728
|
+
// src/scenario-failure.ts
|
|
20729
|
+
function failingScenarioMessage(tc) {
|
|
20730
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20731
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20732
|
+
}
|
|
20733
|
+
|
|
20734
|
+
// src/check.ts
|
|
20735
|
+
var ICON_PASS = "\u2713";
|
|
20736
|
+
var ICON_FAIL = "\u2717";
|
|
20737
|
+
var ICON_SKIP = "\u2298";
|
|
20738
|
+
var ICON_PENDING = "\u23F3";
|
|
20739
|
+
var ICON_WARN = "\u26A0";
|
|
20740
|
+
function buildCheck(args, _deps = {}) {
|
|
20741
|
+
const { testCases, baseline } = args;
|
|
20742
|
+
const summary = {
|
|
20743
|
+
total: testCases.length,
|
|
20744
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20745
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20746
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20747
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20748
|
+
};
|
|
20749
|
+
let regressed = 0;
|
|
20750
|
+
let fixed = 0;
|
|
20751
|
+
if (baseline) {
|
|
20752
|
+
for (const tc of testCases) {
|
|
20753
|
+
const before = baseline.get(tc.id);
|
|
20754
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20755
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20756
|
+
}
|
|
20757
|
+
}
|
|
20758
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20759
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20760
|
+
return a.location.localeCompare(b.location);
|
|
20761
|
+
});
|
|
20762
|
+
return {
|
|
20763
|
+
summary,
|
|
20764
|
+
failures,
|
|
20765
|
+
regressed,
|
|
20766
|
+
fixed,
|
|
20767
|
+
comparedToBaseline: baseline !== void 0
|
|
20768
|
+
};
|
|
20769
|
+
}
|
|
20770
|
+
function toFailure(tc, baseline) {
|
|
20771
|
+
const failedIndexes = new Set(
|
|
20772
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20773
|
+
);
|
|
20774
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20775
|
+
keyword: step.keyword,
|
|
20776
|
+
text: step.text,
|
|
20777
|
+
failed: failedIndexes.has(index)
|
|
20778
|
+
}));
|
|
20779
|
+
return {
|
|
20780
|
+
id: tc.id,
|
|
20781
|
+
scenario: tc.story.scenario,
|
|
20782
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20783
|
+
steps,
|
|
20784
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20785
|
+
covers: tc.story.covers ?? [],
|
|
20786
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20787
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20788
|
+
};
|
|
20789
|
+
}
|
|
20790
|
+
function renderCheck(report, format) {
|
|
20791
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20792
|
+
}
|
|
20793
|
+
function renderCheckText(report) {
|
|
20794
|
+
const { summary, failures } = report;
|
|
20795
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20796
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20797
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20798
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20799
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20800
|
+
if (failures.length === 0) {
|
|
20801
|
+
const lines2 = [headline];
|
|
20802
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20803
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20804
|
+
}
|
|
20805
|
+
lines2.push("All scenarios green.");
|
|
20806
|
+
return lines2.join("\n");
|
|
20807
|
+
}
|
|
20808
|
+
const lines = [headline, ""];
|
|
20809
|
+
for (const f of failures) {
|
|
20810
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20811
|
+
lines.push(` ${f.location}`);
|
|
20812
|
+
for (const step of f.steps) {
|
|
20813
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20814
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20815
|
+
}
|
|
20816
|
+
if (f.errorMessage) {
|
|
20817
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20818
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20819
|
+
}
|
|
20820
|
+
if (f.covers.length > 0) {
|
|
20821
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20822
|
+
}
|
|
20823
|
+
if (f.tickets.length > 0) {
|
|
20824
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20825
|
+
}
|
|
20826
|
+
lines.push("");
|
|
20827
|
+
}
|
|
20828
|
+
if (report.comparedToBaseline) {
|
|
20829
|
+
if (report.regressed > 0) {
|
|
20830
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20831
|
+
}
|
|
20832
|
+
if (report.fixed > 0) {
|
|
20833
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20834
|
+
}
|
|
20835
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20836
|
+
lines.push("No status changes vs. baseline.");
|
|
20837
|
+
}
|
|
20838
|
+
}
|
|
20839
|
+
return lines.join("\n").trimEnd();
|
|
20840
|
+
}
|
|
20841
|
+
|
|
20842
|
+
// src/goal.ts
|
|
20843
|
+
var ACTIVE = ["passed", "failed"];
|
|
20844
|
+
function buildGoal(args, _deps = {}) {
|
|
20845
|
+
const { run, baseline } = args;
|
|
20846
|
+
const cases = run.testCases;
|
|
20847
|
+
const selectors = [
|
|
20848
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20849
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20850
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20851
|
+
];
|
|
20852
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20853
|
+
const regressions = [];
|
|
20854
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20855
|
+
const before = statusMap(baseline);
|
|
20856
|
+
for (const tc of cases) {
|
|
20857
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20858
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20859
|
+
}
|
|
20860
|
+
}
|
|
20861
|
+
}
|
|
20862
|
+
const violations = [];
|
|
20863
|
+
if (baseline && args.enforceRatchet) {
|
|
20864
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20865
|
+
for (const base of baseline.testCases) {
|
|
20866
|
+
const now = current.get(base.id);
|
|
20867
|
+
if (!now) {
|
|
20868
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20869
|
+
continue;
|
|
20870
|
+
}
|
|
20871
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20872
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20873
|
+
}
|
|
20874
|
+
const baseSteps = base.story.steps.length;
|
|
20875
|
+
const nowSteps = now.story.steps.length;
|
|
20876
|
+
if (nowSteps < baseSteps) {
|
|
20877
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20878
|
+
}
|
|
20879
|
+
}
|
|
20880
|
+
}
|
|
20881
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20882
|
+
return {
|
|
20883
|
+
met,
|
|
20884
|
+
requirements,
|
|
20885
|
+
regressions,
|
|
20886
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20887
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20888
|
+
};
|
|
20889
|
+
}
|
|
20890
|
+
function evaluate(selector, matched) {
|
|
20891
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20892
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20893
|
+
return {
|
|
20894
|
+
selector,
|
|
20895
|
+
matched: matched.length,
|
|
20896
|
+
passed,
|
|
20897
|
+
failing,
|
|
20898
|
+
met: matched.length > 0 && failing.length === 0
|
|
20899
|
+
};
|
|
20900
|
+
}
|
|
20901
|
+
function statusMap(run) {
|
|
20902
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20903
|
+
}
|
|
20904
|
+
function renderGoal(report, format) {
|
|
20905
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20906
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20907
|
+
for (const req of report.requirements) {
|
|
20908
|
+
if (req.matched === 0) {
|
|
20909
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20910
|
+
continue;
|
|
20911
|
+
}
|
|
20912
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20913
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20914
|
+
}
|
|
20915
|
+
if (report.regressionsEnforced) {
|
|
20916
|
+
if (report.regressions.length === 0) {
|
|
20917
|
+
lines.push(" regressions: 0");
|
|
20918
|
+
} else {
|
|
20919
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20920
|
+
}
|
|
20921
|
+
}
|
|
20922
|
+
if (report.ratchet.enforced) {
|
|
20923
|
+
if (report.ratchet.violations.length === 0) {
|
|
20924
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20925
|
+
} else {
|
|
20926
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20927
|
+
for (const v of report.ratchet.violations) {
|
|
20928
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20929
|
+
}
|
|
20930
|
+
}
|
|
20931
|
+
}
|
|
20932
|
+
return lines.join("\n");
|
|
20933
|
+
}
|
|
20934
|
+
|
|
20935
|
+
// src/triage.ts
|
|
20936
|
+
function buildTriage(args, _deps = {}) {
|
|
20937
|
+
const { testCases, baseline } = args;
|
|
20938
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20939
|
+
const ranked = failing.map((tc) => {
|
|
20940
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20941
|
+
return {
|
|
20942
|
+
tc,
|
|
20943
|
+
regressed,
|
|
20944
|
+
covers: tc.story.covers ?? []
|
|
20945
|
+
};
|
|
20946
|
+
}).sort((a, b) => {
|
|
20947
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20948
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20949
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20950
|
+
return la.localeCompare(lb);
|
|
20951
|
+
});
|
|
20952
|
+
const items = ranked.map((entry, index) => ({
|
|
20953
|
+
rank: index + 1,
|
|
20954
|
+
id: entry.tc.id,
|
|
20955
|
+
scenario: entry.tc.story.scenario,
|
|
20956
|
+
status: entry.tc.status,
|
|
20957
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20958
|
+
covers: entry.covers,
|
|
20959
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20960
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20961
|
+
regressed: entry.regressed,
|
|
20962
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20963
|
+
}));
|
|
20964
|
+
return {
|
|
20965
|
+
total: testCases.length,
|
|
20966
|
+
failing: failing.length,
|
|
20967
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20968
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20969
|
+
items
|
|
20970
|
+
};
|
|
20971
|
+
}
|
|
20972
|
+
function renderTriage(report, format) {
|
|
20973
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20974
|
+
if (report.items.length === 0) {
|
|
20975
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20976
|
+
}
|
|
20977
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20978
|
+
const lines = [header, ""];
|
|
20979
|
+
for (const item of report.items) {
|
|
20980
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20981
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20982
|
+
lines.push(` ${item.location}`);
|
|
20983
|
+
if (item.errorMessage) {
|
|
20984
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20985
|
+
}
|
|
20986
|
+
if (item.covers.length > 0) {
|
|
20987
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20988
|
+
} else {
|
|
20989
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
20990
|
+
}
|
|
20991
|
+
if (item.tickets.length > 0) {
|
|
20992
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
20993
|
+
}
|
|
20994
|
+
lines.push("");
|
|
20995
|
+
}
|
|
20996
|
+
if (report.needsCovers > 0) {
|
|
20997
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
20998
|
+
}
|
|
20999
|
+
return lines.join("\n").trimEnd();
|
|
21000
|
+
}
|
|
21001
|
+
|
|
20605
21002
|
// src/review/conventions.ts
|
|
20606
21003
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20607
21004
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -21403,6 +21800,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
21403
21800
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
21404
21801
|
markdown: ".md",
|
|
21405
21802
|
"release-manifest": ".release-manifest.md",
|
|
21803
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
21406
21804
|
html: ".html",
|
|
21407
21805
|
"cucumber-html": ".cucumber.html",
|
|
21408
21806
|
junit: ".junit.xml",
|
|
@@ -21625,7 +22023,9 @@ var ReportGenerator = class {
|
|
|
21625
22023
|
permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
|
|
21626
22024
|
ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
|
|
21627
22025
|
traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
|
|
21628
|
-
customRenderers: options.astro?.markdown?.customRenderers
|
|
22026
|
+
customRenderers: options.astro?.markdown?.customRenderers,
|
|
22027
|
+
scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
|
|
22028
|
+
scenarioBadge: options.astro?.markdown?.scenarioBadge
|
|
21629
22029
|
}
|
|
21630
22030
|
},
|
|
21631
22031
|
assetMode: options.assetMode ?? "none",
|
|
@@ -21829,6 +22229,10 @@ var ReportGenerator = class {
|
|
|
21829
22229
|
const formatter = new ReleaseManifestFormatter();
|
|
21830
22230
|
return formatter.format(run);
|
|
21831
22231
|
}
|
|
22232
|
+
case "traceability-matrix": {
|
|
22233
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
22234
|
+
return formatter.format(run);
|
|
22235
|
+
}
|
|
21832
22236
|
case "story-report-json": {
|
|
21833
22237
|
const formatter = new StoryReportJsonFormatter({
|
|
21834
22238
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21909,12 +22313,16 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21909
22313
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21910
22314
|
ScenarioIndexJsonFormatter,
|
|
21911
22315
|
StoryReportJsonFormatter,
|
|
22316
|
+
TraceabilityMatrixFormatter,
|
|
21912
22317
|
adaptJestRun,
|
|
21913
22318
|
adaptPlaywrightRun,
|
|
21914
22319
|
adaptVitestRun,
|
|
21915
22320
|
assertValidRun,
|
|
22321
|
+
buildCheck,
|
|
22322
|
+
buildGoal,
|
|
21916
22323
|
buildHtmlDocEntry,
|
|
21917
22324
|
buildReview,
|
|
22325
|
+
buildTriage,
|
|
21918
22326
|
bundleAssets,
|
|
21919
22327
|
calculateFlakiness,
|
|
21920
22328
|
calculateStability,
|
|
@@ -21964,6 +22372,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21964
22372
|
readPackageVersion,
|
|
21965
22373
|
recordDeployment,
|
|
21966
22374
|
regenerateArtifacts,
|
|
22375
|
+
renderCheck,
|
|
22376
|
+
renderGoal,
|
|
22377
|
+
renderTriage,
|
|
21967
22378
|
resolveAttachment,
|
|
21968
22379
|
resolveAttachments,
|
|
21969
22380
|
resolveTheme,
|
|
@@ -21985,6 +22396,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21985
22396
|
toReleaseManifest,
|
|
21986
22397
|
toScenarioIndex,
|
|
21987
22398
|
toStoryReport,
|
|
22399
|
+
toTraceabilityMatrix,
|
|
21988
22400
|
tryGetActiveOtelContext,
|
|
21989
22401
|
updateHistory,
|
|
21990
22402
|
validateCanonicalRun
|