executable-stories-formatters 0.11.4 → 0.13.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 +812 -13
- package/dist/cli.js.map +1 -1
- package/dist/{index-DF16Xl5i.d.cts → index-CXrzCk9p.d.cts} +10 -1
- package/dist/{index-DF16Xl5i.d.ts → index-CXrzCk9p.d.ts} +10 -1
- package/dist/index.cjs +671 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -6
- package/dist/index.d.ts +280 -6
- package/dist/index.js +662 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/raw-run.schema.json +30 -0
- package/schemas/story-report-v1.json +24 -0
package/dist/index.cjs
CHANGED
|
@@ -55,11 +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,
|
|
65
|
+
buildHtmlDocEntry: () => buildHtmlDocEntry,
|
|
62
66
|
buildReview: () => buildReview,
|
|
67
|
+
buildTriage: () => buildTriage,
|
|
63
68
|
bundleAssets: () => bundleAssets,
|
|
64
69
|
calculateFlakiness: () => calculateFlakiness,
|
|
65
70
|
calculateStability: () => calculateStability,
|
|
@@ -109,6 +114,9 @@ __export(src_exports, {
|
|
|
109
114
|
readPackageVersion: () => readPackageVersion,
|
|
110
115
|
recordDeployment: () => recordDeployment,
|
|
111
116
|
regenerateArtifacts: () => regenerateArtifacts,
|
|
117
|
+
renderCheck: () => renderCheck,
|
|
118
|
+
renderGoal: () => renderGoal,
|
|
119
|
+
renderTriage: () => renderTriage,
|
|
112
120
|
resolveAttachment: () => resolveAttachment,
|
|
113
121
|
resolveAttachments: () => resolveAttachments,
|
|
114
122
|
resolveTheme: () => resolveTheme,
|
|
@@ -130,6 +138,7 @@ __export(src_exports, {
|
|
|
130
138
|
toReleaseManifest: () => toReleaseManifest,
|
|
131
139
|
toScenarioIndex: () => toScenarioIndex,
|
|
132
140
|
toStoryReport: () => toStoryReport,
|
|
141
|
+
toTraceabilityMatrix: () => toTraceabilityMatrix,
|
|
133
142
|
tryGetActiveOtelContext: () => tryGetActiveOtelContext,
|
|
134
143
|
updateHistory: () => updateHistory,
|
|
135
144
|
validateCanonicalRun: () => validateCanonicalRun
|
|
@@ -630,6 +639,14 @@ var CucumberJsonFormatter = class {
|
|
|
630
639
|
}
|
|
631
640
|
const embeddings = [];
|
|
632
641
|
for (const doc of step.docs) {
|
|
642
|
+
if (doc.kind === "html" && doc.content !== void 0) {
|
|
643
|
+
embeddings.push({
|
|
644
|
+
data: Buffer.from(doc.content, "utf8").toString("base64"),
|
|
645
|
+
mime_type: "text/html",
|
|
646
|
+
name: doc.title
|
|
647
|
+
});
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
633
650
|
if (doc.kind !== "screenshot" || !doc.path.startsWith("data:")) {
|
|
634
651
|
continue;
|
|
635
652
|
}
|
|
@@ -654,14 +671,14 @@ var CucumberJsonFormatter = class {
|
|
|
654
671
|
duration: 0
|
|
655
672
|
};
|
|
656
673
|
}
|
|
657
|
-
const
|
|
674
|
+
const statusMap2 = {
|
|
658
675
|
passed: "passed",
|
|
659
676
|
failed: "failed",
|
|
660
677
|
skipped: "skipped",
|
|
661
678
|
pending: "pending"
|
|
662
679
|
};
|
|
663
680
|
const stepResult = {
|
|
664
|
-
status:
|
|
681
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
665
682
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
666
683
|
duration: result.durationMs * 1e6
|
|
667
684
|
};
|
|
@@ -809,6 +826,17 @@ ${doc.markdown}`,
|
|
|
809
826
|
};
|
|
810
827
|
case "screenshot":
|
|
811
828
|
return null;
|
|
829
|
+
case "html":
|
|
830
|
+
if (doc.url !== void 0 || doc.path !== void 0) {
|
|
831
|
+
return {
|
|
832
|
+
doc_string: {
|
|
833
|
+
content: `[${doc.title ?? "Embedded HTML"}](${doc.url ?? doc.path})`,
|
|
834
|
+
content_type: "text/markdown",
|
|
835
|
+
line: 0
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
812
840
|
default:
|
|
813
841
|
return null;
|
|
814
842
|
}
|
|
@@ -908,6 +936,17 @@ function copyDocEntry(entry) {
|
|
|
908
936
|
phase: entry.phase,
|
|
909
937
|
...children
|
|
910
938
|
};
|
|
939
|
+
case "html":
|
|
940
|
+
return {
|
|
941
|
+
kind: "html",
|
|
942
|
+
...entry.path !== void 0 ? { path: entry.path } : {},
|
|
943
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
944
|
+
...entry.content !== void 0 ? { content: entry.content } : {},
|
|
945
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
946
|
+
...entry.height !== void 0 ? { height: entry.height } : {},
|
|
947
|
+
phase: entry.phase,
|
|
948
|
+
...children
|
|
949
|
+
};
|
|
911
950
|
case "custom":
|
|
912
951
|
return {
|
|
913
952
|
kind: "custom",
|
|
@@ -1296,7 +1335,7 @@ function scenarioHasDocs(scenario) {
|
|
|
1296
1335
|
var fs2 = __toESM(require("fs"), 1);
|
|
1297
1336
|
var path3 = __toESM(require("path"), 1);
|
|
1298
1337
|
|
|
1299
|
-
// src/formatters/html/template.ts
|
|
1338
|
+
// src/formatters/html/template-scripts.ts
|
|
1300
1339
|
var JS_THEME = `
|
|
1301
1340
|
// Theme management
|
|
1302
1341
|
function getSystemTheme() {
|
|
@@ -2098,6 +2137,23 @@ function parseMarkdownSections(marked) {
|
|
|
2098
2137
|
});
|
|
2099
2138
|
}
|
|
2100
2139
|
`;
|
|
2140
|
+
var JS_HTML_EMBED = `
|
|
2141
|
+
// Open srcdoc-embedded HTML (doc-html iframes) in a new tab via a blob URL
|
|
2142
|
+
function initHtmlEmbeds() {
|
|
2143
|
+
document.querySelectorAll('.doc-html-open-srcdoc').forEach((btn) => {
|
|
2144
|
+
btn.addEventListener('click', function() {
|
|
2145
|
+
const container = btn.closest('.doc-html');
|
|
2146
|
+
const iframe = container ? container.querySelector('iframe.doc-html-frame') : null;
|
|
2147
|
+
const html = iframe ? iframe.getAttribute('srcdoc') : null;
|
|
2148
|
+
if (!html) return;
|
|
2149
|
+
const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
|
|
2150
|
+
window.open(url, '_blank', 'noopener');
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
`;
|
|
2155
|
+
|
|
2156
|
+
// src/formatters/html/template.ts
|
|
2101
2157
|
function generateScript(options) {
|
|
2102
2158
|
const initCalls = [];
|
|
2103
2159
|
if (options.includeDarkMode) {
|
|
@@ -2115,6 +2171,7 @@ function generateScript(options) {
|
|
|
2115
2171
|
initCalls.push("initHashScroll();");
|
|
2116
2172
|
initCalls.push("initToc();");
|
|
2117
2173
|
initCalls.push("initThemePicker();");
|
|
2174
|
+
initCalls.push("initHtmlEmbeds();");
|
|
2118
2175
|
const initScript = `
|
|
2119
2176
|
// Initialize on load
|
|
2120
2177
|
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -2123,6 +2180,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2123
2180
|
`;
|
|
2124
2181
|
let script = options.includeDarkMode ? JS_THEME : "";
|
|
2125
2182
|
script += JS_CORE;
|
|
2183
|
+
script += JS_HTML_EMBED;
|
|
2126
2184
|
if (options.additionalJs) {
|
|
2127
2185
|
script += options.additionalJs;
|
|
2128
2186
|
}
|
|
@@ -3788,6 +3846,82 @@ body {
|
|
|
3788
3846
|
opacity: 0.8;
|
|
3789
3847
|
}
|
|
3790
3848
|
|
|
3849
|
+
/* ============================================================================
|
|
3850
|
+
Documentation Entries - Embedded HTML
|
|
3851
|
+
============================================================================ */
|
|
3852
|
+
.doc-html {
|
|
3853
|
+
margin-bottom: 0.5rem;
|
|
3854
|
+
border: 1px solid var(--border);
|
|
3855
|
+
border-radius: calc(var(--radius) - 2px);
|
|
3856
|
+
overflow: hidden;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
.doc-html:last-child {
|
|
3860
|
+
margin-bottom: 0;
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
.doc-html-header {
|
|
3864
|
+
display: flex;
|
|
3865
|
+
align-items: center;
|
|
3866
|
+
justify-content: space-between;
|
|
3867
|
+
gap: 0.5rem;
|
|
3868
|
+
padding: 0.375rem 0.75rem;
|
|
3869
|
+
background: var(--muted, transparent);
|
|
3870
|
+
border-bottom: 1px solid var(--border);
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
.doc-html-title {
|
|
3874
|
+
font-size: 0.75rem;
|
|
3875
|
+
font-weight: 600;
|
|
3876
|
+
color: var(--muted-foreground);
|
|
3877
|
+
text-transform: uppercase;
|
|
3878
|
+
letter-spacing: 0.04em;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
.doc-html-open {
|
|
3882
|
+
font-size: 0.875rem;
|
|
3883
|
+
line-height: 1;
|
|
3884
|
+
padding: 0.125rem 0.375rem;
|
|
3885
|
+
border: 1px solid var(--border);
|
|
3886
|
+
border-radius: calc(var(--radius) - 4px);
|
|
3887
|
+
background: transparent;
|
|
3888
|
+
color: var(--muted-foreground);
|
|
3889
|
+
cursor: pointer;
|
|
3890
|
+
text-decoration: none;
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
.doc-html-open:hover {
|
|
3894
|
+
color: var(--foreground);
|
|
3895
|
+
border-color: var(--foreground);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
.doc-html-frame {
|
|
3899
|
+
display: block;
|
|
3900
|
+
width: 100%;
|
|
3901
|
+
border: 0;
|
|
3902
|
+
background: #fff;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
.doc-html-missing {
|
|
3906
|
+
padding: 0.75rem 1rem;
|
|
3907
|
+
border: 1px dashed var(--border);
|
|
3908
|
+
background: var(--muted, transparent);
|
|
3909
|
+
color: var(--muted-foreground);
|
|
3910
|
+
font-size: 0.8125rem;
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
.doc-html-missing-label {
|
|
3914
|
+
font-weight: 600;
|
|
3915
|
+
margin-bottom: 0.25rem;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
.doc-html-missing-path {
|
|
3919
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
3920
|
+
font-size: 0.75rem;
|
|
3921
|
+
word-break: break-all;
|
|
3922
|
+
opacity: 0.8;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3791
3925
|
/* ============================================================================
|
|
3792
3926
|
Documentation Entries - Visual Check
|
|
3793
3927
|
============================================================================ */
|
|
@@ -14086,6 +14220,36 @@ function renderDocVideo(entry, deps) {
|
|
|
14086
14220
|
${captionHtml}
|
|
14087
14221
|
</div>`;
|
|
14088
14222
|
}
|
|
14223
|
+
function resolveHtmlSource(entry, deps) {
|
|
14224
|
+
if (entry.url !== void 0) return { mode: "src", value: entry.url };
|
|
14225
|
+
if (entry.content !== void 0) return { mode: "srcdoc", value: entry.content };
|
|
14226
|
+
const filePath = entry.path ?? "";
|
|
14227
|
+
if (/^https?:/i.test(filePath)) return { mode: "src", value: filePath };
|
|
14228
|
+
const inlined = deps.readHtmlFile?.(filePath);
|
|
14229
|
+
if (inlined !== void 0) return { mode: "srcdoc", value: inlined };
|
|
14230
|
+
const isAbsoluteFsPath = /^(?:[/\\]|[A-Za-z]:[/\\])/.test(filePath);
|
|
14231
|
+
if (deps.readHtmlFile && isAbsoluteFsPath) return { mode: "missing", value: filePath };
|
|
14232
|
+
return { mode: "src", value: filePath };
|
|
14233
|
+
}
|
|
14234
|
+
function renderDocHtml(entry, deps) {
|
|
14235
|
+
const source = resolveHtmlSource(entry, deps);
|
|
14236
|
+
if (source.mode === "missing") {
|
|
14237
|
+
return `<div class="doc-html doc-html-missing">
|
|
14238
|
+
<div class="doc-html-missing-label">HTML unavailable</div>
|
|
14239
|
+
<div class="doc-html-missing-path">${deps.escapeHtml(source.value)}</div>
|
|
14240
|
+
</div>`;
|
|
14241
|
+
}
|
|
14242
|
+
const heightCss = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
|
|
14243
|
+
const frame = `<iframe class="doc-html-frame" sandbox="allow-scripts" loading="lazy" style="height: ${deps.escapeHtml(heightCss)};" title="${deps.escapeHtml(entry.title ?? "Embedded HTML")}" ${source.mode}="${deps.escapeHtml(source.value)}"></iframe>`;
|
|
14244
|
+
const openBtn = source.mode === "src" ? `<a class="doc-html-open" href="${deps.escapeHtml(source.value)}" target="_blank" rel="noopener noreferrer" title="Open in new tab" aria-label="Open in new tab">↗</a>` : `<button type="button" class="doc-html-open doc-html-open-srcdoc" title="Open in new tab" aria-label="Open in new tab">↗</button>`;
|
|
14245
|
+
return `<div class="doc-html">
|
|
14246
|
+
<div class="doc-html-header">
|
|
14247
|
+
<span class="doc-html-title">${deps.escapeHtml(entry.title ?? "HTML")}</span>
|
|
14248
|
+
${openBtn}
|
|
14249
|
+
</div>
|
|
14250
|
+
${frame}
|
|
14251
|
+
</div>`;
|
|
14252
|
+
}
|
|
14089
14253
|
function renderDocCustom(entry, deps) {
|
|
14090
14254
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
14091
14255
|
const data = entry.data;
|
|
@@ -14142,6 +14306,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14142
14306
|
case "video":
|
|
14143
14307
|
html = renderDocVideo(entry, deps);
|
|
14144
14308
|
break;
|
|
14309
|
+
case "html":
|
|
14310
|
+
html = renderDocHtml(entry, deps);
|
|
14311
|
+
break;
|
|
14145
14312
|
case "custom":
|
|
14146
14313
|
html = renderDocCustom(entry, deps);
|
|
14147
14314
|
break;
|
|
@@ -14713,6 +14880,21 @@ function readScreenshotAsDataUri(filePath) {
|
|
|
14713
14880
|
return void 0;
|
|
14714
14881
|
}
|
|
14715
14882
|
}
|
|
14883
|
+
var HTML_INLINE_WARN_BYTES = 1024 * 1024;
|
|
14884
|
+
function readHtmlFileContent(filePath) {
|
|
14885
|
+
try {
|
|
14886
|
+
if (!fs2.existsSync(filePath)) return void 0;
|
|
14887
|
+
const buf = fs2.readFileSync(filePath);
|
|
14888
|
+
if (buf.byteLength > HTML_INLINE_WARN_BYTES) {
|
|
14889
|
+
console.warn(
|
|
14890
|
+
`[executable-stories] Inlining large HTML file (${Math.round(buf.byteLength / 1024)} KiB) into the report: ${filePath}. Consider --asset-mode copy.`
|
|
14891
|
+
);
|
|
14892
|
+
}
|
|
14893
|
+
return buf.toString("utf8");
|
|
14894
|
+
} catch {
|
|
14895
|
+
return void 0;
|
|
14896
|
+
}
|
|
14897
|
+
}
|
|
14716
14898
|
function normalizeOptions(options = {}) {
|
|
14717
14899
|
return {
|
|
14718
14900
|
title: options.title ?? "Test Results",
|
|
@@ -14720,6 +14902,7 @@ function normalizeOptions(options = {}) {
|
|
|
14720
14902
|
searchable: options.searchable ?? true,
|
|
14721
14903
|
startCollapsed: options.startCollapsed ?? false,
|
|
14722
14904
|
embedScreenshots: options.embedScreenshots ?? true,
|
|
14905
|
+
embedHtmlFiles: options.embedHtmlFiles ?? true,
|
|
14723
14906
|
syntaxHighlighting: options.syntaxHighlighting ?? true,
|
|
14724
14907
|
mermaidEnabled: options.mermaidEnabled ?? true,
|
|
14725
14908
|
markdownEnabled: options.markdownEnabled ?? true,
|
|
@@ -14738,7 +14921,10 @@ function createHtmlFormatter(options = {}) {
|
|
|
14738
14921
|
markdownEnabled: opts.markdownEnabled,
|
|
14739
14922
|
mermaidEnabled: opts.mermaidEnabled,
|
|
14740
14923
|
embedScreenshots: opts.embedScreenshots,
|
|
14741
|
-
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath)
|
|
14924
|
+
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath),
|
|
14925
|
+
// When html-file inlining is off (e.g. --asset-mode copy), omit the read
|
|
14926
|
+
// hook so doc-html iframes keep their src path for the asset bundler.
|
|
14927
|
+
...opts.embedHtmlFiles ? { readHtmlFile: (filePath) => readHtmlFileContent(filePath) } : {}
|
|
14742
14928
|
};
|
|
14743
14929
|
const renderDocs = (docs, containerClass) => {
|
|
14744
14930
|
if (!docs || docs.length === 0) return "";
|
|
@@ -15030,6 +15216,8 @@ var JUnitFormatter = class {
|
|
|
15030
15216
|
}
|
|
15031
15217
|
case "screenshot":
|
|
15032
15218
|
return `${indent}Screenshot: ${entry.alt ?? entry.path}`;
|
|
15219
|
+
case "html":
|
|
15220
|
+
return `${indent}HTML: ${entry.title ?? "Embedded HTML"} (${entry.url ?? entry.path ?? "inline"})`;
|
|
15033
15221
|
case "custom": {
|
|
15034
15222
|
const dataStr = JSON.stringify(entry.data, null, 2);
|
|
15035
15223
|
const lines = [];
|
|
@@ -15481,6 +15669,25 @@ var MarkdownFormatter = class {
|
|
|
15481
15669
|
lines.push(`${indent}`);
|
|
15482
15670
|
break;
|
|
15483
15671
|
}
|
|
15672
|
+
case "html": {
|
|
15673
|
+
const htmlLabel = entry.title ?? "Embedded HTML";
|
|
15674
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
15675
|
+
lines.push(`${indent}[${htmlLabel}](${entry.url ?? entry.path})`);
|
|
15676
|
+
break;
|
|
15677
|
+
}
|
|
15678
|
+
lines.push(`${indent}<details>`);
|
|
15679
|
+
lines.push(`${indent}<summary>${htmlLabel}</summary>`);
|
|
15680
|
+
lines.push(`${indent}`);
|
|
15681
|
+
lines.push(`${indent}\`\`\`html`);
|
|
15682
|
+
for (const line of (entry.content ?? "").split("\n")) {
|
|
15683
|
+
lines.push(`${indent}${line}`);
|
|
15684
|
+
}
|
|
15685
|
+
lines.push(`${indent}\`\`\``);
|
|
15686
|
+
lines.push(`${indent}`);
|
|
15687
|
+
lines.push(`${indent}</details>`);
|
|
15688
|
+
lines.push(`${indent}`);
|
|
15689
|
+
break;
|
|
15690
|
+
}
|
|
15484
15691
|
case "custom":
|
|
15485
15692
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15486
15693
|
const data = entry.data;
|
|
@@ -15655,6 +15862,132 @@ function escapePipe(value) {
|
|
|
15655
15862
|
return value.replace(/\|/g, "\\|");
|
|
15656
15863
|
}
|
|
15657
15864
|
|
|
15865
|
+
// src/formatters/traceability-matrix.ts
|
|
15866
|
+
var TraceabilityMatrixFormatter = class {
|
|
15867
|
+
format(run) {
|
|
15868
|
+
const matrix = toTraceabilityMatrix(run);
|
|
15869
|
+
const lines = [];
|
|
15870
|
+
lines.push("# Traceability Matrix");
|
|
15871
|
+
lines.push("");
|
|
15872
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
15873
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
15874
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
15875
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
15876
|
+
lines.push("");
|
|
15877
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
15878
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
15879
|
+
lines.push(
|
|
15880
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
15881
|
+
);
|
|
15882
|
+
lines.push("");
|
|
15883
|
+
for (const req of matrix.requirements) {
|
|
15884
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
15885
|
+
lines.push(`## ${heading2}`);
|
|
15886
|
+
lines.push("");
|
|
15887
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
15888
|
+
if (req.covers.length > 0) {
|
|
15889
|
+
lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
|
|
15890
|
+
}
|
|
15891
|
+
lines.push("");
|
|
15892
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
15893
|
+
lines.push("| --- | --- | --- | --- |");
|
|
15894
|
+
for (const scenario of req.scenarios) {
|
|
15895
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15896
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
|
|
15897
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
15898
|
+
}
|
|
15899
|
+
lines.push("");
|
|
15900
|
+
}
|
|
15901
|
+
if (matrix.untraced.length > 0) {
|
|
15902
|
+
lines.push("## Untraced scenarios");
|
|
15903
|
+
lines.push("");
|
|
15904
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
15905
|
+
lines.push("");
|
|
15906
|
+
lines.push("| Status | Scenario | Source |");
|
|
15907
|
+
lines.push("| --- | --- | --- |");
|
|
15908
|
+
for (const scenario of matrix.untraced) {
|
|
15909
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15910
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
15911
|
+
}
|
|
15912
|
+
lines.push("");
|
|
15913
|
+
}
|
|
15914
|
+
return lines.join("\n").trimEnd();
|
|
15915
|
+
}
|
|
15916
|
+
};
|
|
15917
|
+
function toTraceabilityMatrix(run) {
|
|
15918
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
15919
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
15920
|
+
const untraced = [];
|
|
15921
|
+
for (const tc of sorted) {
|
|
15922
|
+
const tickets = tc.story.tickets ?? [];
|
|
15923
|
+
if (tickets.length === 0) {
|
|
15924
|
+
untraced.push({
|
|
15925
|
+
id: tc.id,
|
|
15926
|
+
title: tc.story.scenario,
|
|
15927
|
+
status: tc.status,
|
|
15928
|
+
sourceFile: tc.sourceFile,
|
|
15929
|
+
sourceLine: tc.sourceLine
|
|
15930
|
+
});
|
|
15931
|
+
continue;
|
|
15932
|
+
}
|
|
15933
|
+
for (const ticket of tickets) {
|
|
15934
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
15935
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
15936
|
+
entry.cases.push(tc);
|
|
15937
|
+
byTicket.set(ticket.id, entry);
|
|
15938
|
+
}
|
|
15939
|
+
}
|
|
15940
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
15941
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
15942
|
+
id: tc.id,
|
|
15943
|
+
title: tc.story.scenario,
|
|
15944
|
+
status: tc.status,
|
|
15945
|
+
sourceFile: tc.sourceFile,
|
|
15946
|
+
sourceLine: tc.sourceLine,
|
|
15947
|
+
covers: tc.story.covers ?? []
|
|
15948
|
+
}));
|
|
15949
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
15950
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
15951
|
+
});
|
|
15952
|
+
return {
|
|
15953
|
+
schemaVersion: "1.0",
|
|
15954
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15955
|
+
run: {
|
|
15956
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
15957
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
15958
|
+
gitSha: run.gitSha,
|
|
15959
|
+
branch: run.ci?.branch
|
|
15960
|
+
},
|
|
15961
|
+
summary: {
|
|
15962
|
+
requirements: requirements.length,
|
|
15963
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
15964
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
15965
|
+
scenarios: run.testCases.length,
|
|
15966
|
+
untracedScenarios: untraced.length
|
|
15967
|
+
},
|
|
15968
|
+
requirements,
|
|
15969
|
+
untraced
|
|
15970
|
+
};
|
|
15971
|
+
}
|
|
15972
|
+
function requirementStatus(cases) {
|
|
15973
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
15974
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
15975
|
+
return "incomplete";
|
|
15976
|
+
}
|
|
15977
|
+
function renderRequirementStatus(status) {
|
|
15978
|
+
switch (status) {
|
|
15979
|
+
case "verified":
|
|
15980
|
+
return "verified (all scenarios passed)";
|
|
15981
|
+
case "failing":
|
|
15982
|
+
return "failing (a scenario failed)";
|
|
15983
|
+
default:
|
|
15984
|
+
return "incomplete (no scenario passed yet)";
|
|
15985
|
+
}
|
|
15986
|
+
}
|
|
15987
|
+
function escapePipe2(value) {
|
|
15988
|
+
return value.replace(/\|/g, "\\|");
|
|
15989
|
+
}
|
|
15990
|
+
|
|
15658
15991
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
15659
15992
|
function extractFeatureName(testCases, uri) {
|
|
15660
15993
|
for (const tc of testCases) {
|
|
@@ -16724,6 +17057,8 @@ function formatDocEntry(doc) {
|
|
|
16724
17057
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16725
17058
|
case "video":
|
|
16726
17059
|
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17060
|
+
case "html":
|
|
17061
|
+
return `${doc.title ? `${escapeHtml2(doc.title)}: ` : ""}${escapeHtml2(doc.url ?? doc.path ?? "(inline html)")}`;
|
|
16727
17062
|
case "custom":
|
|
16728
17063
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
16729
17064
|
}
|
|
@@ -17182,6 +17517,8 @@ function formatDocEntry2(doc) {
|
|
|
17182
17517
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17183
17518
|
case "video":
|
|
17184
17519
|
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
17520
|
+
case "html":
|
|
17521
|
+
return `${doc.title ? `${doc.title}: ` : ""}${doc.url ?? doc.path ?? "(inline html)"}`;
|
|
17185
17522
|
case "custom":
|
|
17186
17523
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17187
17524
|
}
|
|
@@ -17331,7 +17668,7 @@ var path5 = __toESM(require("path"), 1);
|
|
|
17331
17668
|
function scanHtmlAssets(html) {
|
|
17332
17669
|
const seen = /* @__PURE__ */ new Set();
|
|
17333
17670
|
const patterns = [
|
|
17334
|
-
/<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
17671
|
+
/<(?:img|video|iframe)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
17335
17672
|
/<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
|
|
17336
17673
|
/<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
|
|
17337
17674
|
];
|
|
@@ -17410,7 +17747,7 @@ function bundleAssets(htmlPath, options = {}) {
|
|
|
17410
17747
|
function replaceAssetRef(html, original, replacement) {
|
|
17411
17748
|
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17412
17749
|
const srcPattern = new RegExp(
|
|
17413
|
-
`(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
17750
|
+
`(<(?:img|video|iframe)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
17414
17751
|
"g"
|
|
17415
17752
|
);
|
|
17416
17753
|
html = html.replace(srcPattern, `$1${replacement}$2`);
|
|
@@ -17798,6 +18135,21 @@ ${tc.errorStack}` : "");
|
|
|
17798
18135
|
])
|
|
17799
18136
|
);
|
|
17800
18137
|
break;
|
|
18138
|
+
case "html":
|
|
18139
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
18140
|
+
const target = entry.url ?? entry.path ?? "";
|
|
18141
|
+
content.push(
|
|
18142
|
+
paragraph([
|
|
18143
|
+
text(entry.title ?? "Embedded HTML", strong()),
|
|
18144
|
+
text(": "),
|
|
18145
|
+
link(target, target)
|
|
18146
|
+
])
|
|
18147
|
+
);
|
|
18148
|
+
break;
|
|
18149
|
+
}
|
|
18150
|
+
content.push(paragraph([text(entry.title ?? "Embedded HTML", strong())]));
|
|
18151
|
+
content.push(codeBlock(entry.content ?? "", "html"));
|
|
18152
|
+
break;
|
|
17801
18153
|
case "custom":
|
|
17802
18154
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
17803
18155
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -19515,6 +19867,27 @@ function resolveTraceUrl(template, traceId) {
|
|
|
19515
19867
|
return template.replace(/\{traceId\}/g, traceId);
|
|
19516
19868
|
}
|
|
19517
19869
|
|
|
19870
|
+
// src/utils/doc-builders.ts
|
|
19871
|
+
function buildHtmlDocEntry(options) {
|
|
19872
|
+
const sources = [options.path, options.url, options.content].filter(
|
|
19873
|
+
(v) => v !== void 0
|
|
19874
|
+
);
|
|
19875
|
+
if (sources.length !== 1) {
|
|
19876
|
+
throw new Error(
|
|
19877
|
+
"story.html() requires exactly one of path, url, or content"
|
|
19878
|
+
);
|
|
19879
|
+
}
|
|
19880
|
+
return {
|
|
19881
|
+
kind: "html",
|
|
19882
|
+
path: options.path,
|
|
19883
|
+
url: options.url,
|
|
19884
|
+
content: options.content,
|
|
19885
|
+
title: options.title,
|
|
19886
|
+
height: options.height,
|
|
19887
|
+
phase: "runtime"
|
|
19888
|
+
};
|
|
19889
|
+
}
|
|
19890
|
+
|
|
19518
19891
|
// src/notifiers/slack.ts
|
|
19519
19892
|
function truncate(text2, maxLen) {
|
|
19520
19893
|
if (text2.length <= maxLen) return text2;
|
|
@@ -20363,6 +20736,280 @@ function collectDocKinds(testCase) {
|
|
|
20363
20736
|
return [...kinds].sort();
|
|
20364
20737
|
}
|
|
20365
20738
|
|
|
20739
|
+
// src/scenario-failure.ts
|
|
20740
|
+
function failingScenarioMessage(tc) {
|
|
20741
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20742
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20743
|
+
}
|
|
20744
|
+
|
|
20745
|
+
// src/check.ts
|
|
20746
|
+
var ICON_PASS = "\u2713";
|
|
20747
|
+
var ICON_FAIL = "\u2717";
|
|
20748
|
+
var ICON_SKIP = "\u2298";
|
|
20749
|
+
var ICON_PENDING = "\u23F3";
|
|
20750
|
+
var ICON_WARN = "\u26A0";
|
|
20751
|
+
function buildCheck(args, _deps = {}) {
|
|
20752
|
+
const { testCases, baseline } = args;
|
|
20753
|
+
const summary = {
|
|
20754
|
+
total: testCases.length,
|
|
20755
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20756
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20757
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20758
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20759
|
+
};
|
|
20760
|
+
let regressed = 0;
|
|
20761
|
+
let fixed = 0;
|
|
20762
|
+
if (baseline) {
|
|
20763
|
+
for (const tc of testCases) {
|
|
20764
|
+
const before = baseline.get(tc.id);
|
|
20765
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20766
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20767
|
+
}
|
|
20768
|
+
}
|
|
20769
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20770
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20771
|
+
return a.location.localeCompare(b.location);
|
|
20772
|
+
});
|
|
20773
|
+
return {
|
|
20774
|
+
summary,
|
|
20775
|
+
failures,
|
|
20776
|
+
regressed,
|
|
20777
|
+
fixed,
|
|
20778
|
+
comparedToBaseline: baseline !== void 0
|
|
20779
|
+
};
|
|
20780
|
+
}
|
|
20781
|
+
function toFailure(tc, baseline) {
|
|
20782
|
+
const failedIndexes = new Set(
|
|
20783
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20784
|
+
);
|
|
20785
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20786
|
+
keyword: step.keyword,
|
|
20787
|
+
text: step.text,
|
|
20788
|
+
failed: failedIndexes.has(index)
|
|
20789
|
+
}));
|
|
20790
|
+
return {
|
|
20791
|
+
id: tc.id,
|
|
20792
|
+
scenario: tc.story.scenario,
|
|
20793
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20794
|
+
steps,
|
|
20795
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20796
|
+
covers: tc.story.covers ?? [],
|
|
20797
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20798
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20799
|
+
};
|
|
20800
|
+
}
|
|
20801
|
+
function renderCheck(report, format) {
|
|
20802
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20803
|
+
}
|
|
20804
|
+
function renderCheckText(report) {
|
|
20805
|
+
const { summary, failures } = report;
|
|
20806
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20807
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20808
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20809
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20810
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20811
|
+
if (failures.length === 0) {
|
|
20812
|
+
const lines2 = [headline];
|
|
20813
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20814
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20815
|
+
}
|
|
20816
|
+
lines2.push("All scenarios green.");
|
|
20817
|
+
return lines2.join("\n");
|
|
20818
|
+
}
|
|
20819
|
+
const lines = [headline, ""];
|
|
20820
|
+
for (const f of failures) {
|
|
20821
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20822
|
+
lines.push(` ${f.location}`);
|
|
20823
|
+
for (const step of f.steps) {
|
|
20824
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20825
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20826
|
+
}
|
|
20827
|
+
if (f.errorMessage) {
|
|
20828
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20829
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20830
|
+
}
|
|
20831
|
+
if (f.covers.length > 0) {
|
|
20832
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20833
|
+
}
|
|
20834
|
+
if (f.tickets.length > 0) {
|
|
20835
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20836
|
+
}
|
|
20837
|
+
lines.push("");
|
|
20838
|
+
}
|
|
20839
|
+
if (report.comparedToBaseline) {
|
|
20840
|
+
if (report.regressed > 0) {
|
|
20841
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20842
|
+
}
|
|
20843
|
+
if (report.fixed > 0) {
|
|
20844
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20845
|
+
}
|
|
20846
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20847
|
+
lines.push("No status changes vs. baseline.");
|
|
20848
|
+
}
|
|
20849
|
+
}
|
|
20850
|
+
return lines.join("\n").trimEnd();
|
|
20851
|
+
}
|
|
20852
|
+
|
|
20853
|
+
// src/goal.ts
|
|
20854
|
+
var ACTIVE = ["passed", "failed"];
|
|
20855
|
+
function buildGoal(args, _deps = {}) {
|
|
20856
|
+
const { run, baseline } = args;
|
|
20857
|
+
const cases = run.testCases;
|
|
20858
|
+
const selectors = [
|
|
20859
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20860
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20861
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20862
|
+
];
|
|
20863
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20864
|
+
const regressions = [];
|
|
20865
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20866
|
+
const before = statusMap(baseline);
|
|
20867
|
+
for (const tc of cases) {
|
|
20868
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20869
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20870
|
+
}
|
|
20871
|
+
}
|
|
20872
|
+
}
|
|
20873
|
+
const violations = [];
|
|
20874
|
+
if (baseline && args.enforceRatchet) {
|
|
20875
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20876
|
+
for (const base of baseline.testCases) {
|
|
20877
|
+
const now = current.get(base.id);
|
|
20878
|
+
if (!now) {
|
|
20879
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20880
|
+
continue;
|
|
20881
|
+
}
|
|
20882
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20883
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20884
|
+
}
|
|
20885
|
+
const baseSteps = base.story.steps.length;
|
|
20886
|
+
const nowSteps = now.story.steps.length;
|
|
20887
|
+
if (nowSteps < baseSteps) {
|
|
20888
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20889
|
+
}
|
|
20890
|
+
}
|
|
20891
|
+
}
|
|
20892
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20893
|
+
return {
|
|
20894
|
+
met,
|
|
20895
|
+
requirements,
|
|
20896
|
+
regressions,
|
|
20897
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20898
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20899
|
+
};
|
|
20900
|
+
}
|
|
20901
|
+
function evaluate(selector, matched) {
|
|
20902
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20903
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20904
|
+
return {
|
|
20905
|
+
selector,
|
|
20906
|
+
matched: matched.length,
|
|
20907
|
+
passed,
|
|
20908
|
+
failing,
|
|
20909
|
+
met: matched.length > 0 && failing.length === 0
|
|
20910
|
+
};
|
|
20911
|
+
}
|
|
20912
|
+
function statusMap(run) {
|
|
20913
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20914
|
+
}
|
|
20915
|
+
function renderGoal(report, format) {
|
|
20916
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20917
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20918
|
+
for (const req of report.requirements) {
|
|
20919
|
+
if (req.matched === 0) {
|
|
20920
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20921
|
+
continue;
|
|
20922
|
+
}
|
|
20923
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20924
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20925
|
+
}
|
|
20926
|
+
if (report.regressionsEnforced) {
|
|
20927
|
+
if (report.regressions.length === 0) {
|
|
20928
|
+
lines.push(" regressions: 0");
|
|
20929
|
+
} else {
|
|
20930
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20931
|
+
}
|
|
20932
|
+
}
|
|
20933
|
+
if (report.ratchet.enforced) {
|
|
20934
|
+
if (report.ratchet.violations.length === 0) {
|
|
20935
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20936
|
+
} else {
|
|
20937
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20938
|
+
for (const v of report.ratchet.violations) {
|
|
20939
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20940
|
+
}
|
|
20941
|
+
}
|
|
20942
|
+
}
|
|
20943
|
+
return lines.join("\n");
|
|
20944
|
+
}
|
|
20945
|
+
|
|
20946
|
+
// src/triage.ts
|
|
20947
|
+
function buildTriage(args, _deps = {}) {
|
|
20948
|
+
const { testCases, baseline } = args;
|
|
20949
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20950
|
+
const ranked = failing.map((tc) => {
|
|
20951
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20952
|
+
return {
|
|
20953
|
+
tc,
|
|
20954
|
+
regressed,
|
|
20955
|
+
covers: tc.story.covers ?? []
|
|
20956
|
+
};
|
|
20957
|
+
}).sort((a, b) => {
|
|
20958
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20959
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20960
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20961
|
+
return la.localeCompare(lb);
|
|
20962
|
+
});
|
|
20963
|
+
const items = ranked.map((entry, index) => ({
|
|
20964
|
+
rank: index + 1,
|
|
20965
|
+
id: entry.tc.id,
|
|
20966
|
+
scenario: entry.tc.story.scenario,
|
|
20967
|
+
status: entry.tc.status,
|
|
20968
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20969
|
+
covers: entry.covers,
|
|
20970
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20971
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20972
|
+
regressed: entry.regressed,
|
|
20973
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20974
|
+
}));
|
|
20975
|
+
return {
|
|
20976
|
+
total: testCases.length,
|
|
20977
|
+
failing: failing.length,
|
|
20978
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20979
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20980
|
+
items
|
|
20981
|
+
};
|
|
20982
|
+
}
|
|
20983
|
+
function renderTriage(report, format) {
|
|
20984
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20985
|
+
if (report.items.length === 0) {
|
|
20986
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20987
|
+
}
|
|
20988
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20989
|
+
const lines = [header, ""];
|
|
20990
|
+
for (const item of report.items) {
|
|
20991
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20992
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20993
|
+
lines.push(` ${item.location}`);
|
|
20994
|
+
if (item.errorMessage) {
|
|
20995
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20996
|
+
}
|
|
20997
|
+
if (item.covers.length > 0) {
|
|
20998
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20999
|
+
} else {
|
|
21000
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
21001
|
+
}
|
|
21002
|
+
if (item.tickets.length > 0) {
|
|
21003
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
21004
|
+
}
|
|
21005
|
+
lines.push("");
|
|
21006
|
+
}
|
|
21007
|
+
if (report.needsCovers > 0) {
|
|
21008
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
21009
|
+
}
|
|
21010
|
+
return lines.join("\n").trimEnd();
|
|
21011
|
+
}
|
|
21012
|
+
|
|
20366
21013
|
// src/review/conventions.ts
|
|
20367
21014
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20368
21015
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -21164,6 +21811,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
21164
21811
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
21165
21812
|
markdown: ".md",
|
|
21166
21813
|
"release-manifest": ".release-manifest.md",
|
|
21814
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
21167
21815
|
html: ".html",
|
|
21168
21816
|
"cucumber-html": ".cucumber.html",
|
|
21169
21817
|
junit: ".junit.xml",
|
|
@@ -21324,6 +21972,9 @@ var ReportGenerator = class {
|
|
|
21324
21972
|
searchable: options.html?.searchable ?? true,
|
|
21325
21973
|
startCollapsed: options.html?.startCollapsed ?? false,
|
|
21326
21974
|
embedScreenshots: options.html?.embedScreenshots ?? true,
|
|
21975
|
+
// Under "copy" asset mode local html files become hashed assets with
|
|
21976
|
+
// an iframe src instead of being inlined into the report.
|
|
21977
|
+
embedHtmlFiles: options.html?.embedHtmlFiles ?? (options.assetMode ?? "none") !== "copy",
|
|
21327
21978
|
syntaxHighlighting: options.html?.syntaxHighlighting ?? true,
|
|
21328
21979
|
mermaidEnabled: options.html?.mermaidEnabled ?? true,
|
|
21329
21980
|
markdownEnabled: options.html?.markdownEnabled ?? true,
|
|
@@ -21499,6 +22150,7 @@ var ReportGenerator = class {
|
|
|
21499
22150
|
searchable: this.options.html.searchable,
|
|
21500
22151
|
startCollapsed: this.options.html.startCollapsed,
|
|
21501
22152
|
embedScreenshots: this.options.html.embedScreenshots,
|
|
22153
|
+
embedHtmlFiles: this.options.html.embedHtmlFiles,
|
|
21502
22154
|
syntaxHighlighting: this.options.html.syntaxHighlighting,
|
|
21503
22155
|
mermaidEnabled: this.options.html.mermaidEnabled,
|
|
21504
22156
|
markdownEnabled: this.options.html.markdownEnabled,
|
|
@@ -21586,6 +22238,10 @@ var ReportGenerator = class {
|
|
|
21586
22238
|
const formatter = new ReleaseManifestFormatter();
|
|
21587
22239
|
return formatter.format(run);
|
|
21588
22240
|
}
|
|
22241
|
+
case "traceability-matrix": {
|
|
22242
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
22243
|
+
return formatter.format(run);
|
|
22244
|
+
}
|
|
21589
22245
|
case "story-report-json": {
|
|
21590
22246
|
const formatter = new StoryReportJsonFormatter({
|
|
21591
22247
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21666,11 +22322,16 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21666
22322
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21667
22323
|
ScenarioIndexJsonFormatter,
|
|
21668
22324
|
StoryReportJsonFormatter,
|
|
22325
|
+
TraceabilityMatrixFormatter,
|
|
21669
22326
|
adaptJestRun,
|
|
21670
22327
|
adaptPlaywrightRun,
|
|
21671
22328
|
adaptVitestRun,
|
|
21672
22329
|
assertValidRun,
|
|
22330
|
+
buildCheck,
|
|
22331
|
+
buildGoal,
|
|
22332
|
+
buildHtmlDocEntry,
|
|
21673
22333
|
buildReview,
|
|
22334
|
+
buildTriage,
|
|
21674
22335
|
bundleAssets,
|
|
21675
22336
|
calculateFlakiness,
|
|
21676
22337
|
calculateStability,
|
|
@@ -21720,6 +22381,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21720
22381
|
readPackageVersion,
|
|
21721
22382
|
recordDeployment,
|
|
21722
22383
|
regenerateArtifacts,
|
|
22384
|
+
renderCheck,
|
|
22385
|
+
renderGoal,
|
|
22386
|
+
renderTriage,
|
|
21723
22387
|
resolveAttachment,
|
|
21724
22388
|
resolveAttachments,
|
|
21725
22389
|
resolveTheme,
|
|
@@ -21741,6 +22405,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21741
22405
|
toReleaseManifest,
|
|
21742
22406
|
toScenarioIndex,
|
|
21743
22407
|
toStoryReport,
|
|
22408
|
+
toTraceabilityMatrix,
|
|
21744
22409
|
tryGetActiveOtelContext,
|
|
21745
22410
|
updateHistory,
|
|
21746
22411
|
validateCanonicalRun
|