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.js
CHANGED
|
@@ -494,6 +494,14 @@ var CucumberJsonFormatter = class {
|
|
|
494
494
|
}
|
|
495
495
|
const embeddings = [];
|
|
496
496
|
for (const doc of step.docs) {
|
|
497
|
+
if (doc.kind === "html" && doc.content !== void 0) {
|
|
498
|
+
embeddings.push({
|
|
499
|
+
data: Buffer.from(doc.content, "utf8").toString("base64"),
|
|
500
|
+
mime_type: "text/html",
|
|
501
|
+
name: doc.title
|
|
502
|
+
});
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
497
505
|
if (doc.kind !== "screenshot" || !doc.path.startsWith("data:")) {
|
|
498
506
|
continue;
|
|
499
507
|
}
|
|
@@ -518,14 +526,14 @@ var CucumberJsonFormatter = class {
|
|
|
518
526
|
duration: 0
|
|
519
527
|
};
|
|
520
528
|
}
|
|
521
|
-
const
|
|
529
|
+
const statusMap2 = {
|
|
522
530
|
passed: "passed",
|
|
523
531
|
failed: "failed",
|
|
524
532
|
skipped: "skipped",
|
|
525
533
|
pending: "pending"
|
|
526
534
|
};
|
|
527
535
|
const stepResult = {
|
|
528
|
-
status:
|
|
536
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
529
537
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
530
538
|
duration: result.durationMs * 1e6
|
|
531
539
|
};
|
|
@@ -673,6 +681,17 @@ ${doc.markdown}`,
|
|
|
673
681
|
};
|
|
674
682
|
case "screenshot":
|
|
675
683
|
return null;
|
|
684
|
+
case "html":
|
|
685
|
+
if (doc.url !== void 0 || doc.path !== void 0) {
|
|
686
|
+
return {
|
|
687
|
+
doc_string: {
|
|
688
|
+
content: `[${doc.title ?? "Embedded HTML"}](${doc.url ?? doc.path})`,
|
|
689
|
+
content_type: "text/markdown",
|
|
690
|
+
line: 0
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
676
695
|
default:
|
|
677
696
|
return null;
|
|
678
697
|
}
|
|
@@ -772,6 +791,17 @@ function copyDocEntry(entry) {
|
|
|
772
791
|
phase: entry.phase,
|
|
773
792
|
...children
|
|
774
793
|
};
|
|
794
|
+
case "html":
|
|
795
|
+
return {
|
|
796
|
+
kind: "html",
|
|
797
|
+
...entry.path !== void 0 ? { path: entry.path } : {},
|
|
798
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
799
|
+
...entry.content !== void 0 ? { content: entry.content } : {},
|
|
800
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
801
|
+
...entry.height !== void 0 ? { height: entry.height } : {},
|
|
802
|
+
phase: entry.phase,
|
|
803
|
+
...children
|
|
804
|
+
};
|
|
775
805
|
case "custom":
|
|
776
806
|
return {
|
|
777
807
|
kind: "custom",
|
|
@@ -1160,7 +1190,7 @@ function scenarioHasDocs(scenario) {
|
|
|
1160
1190
|
import * as fs2 from "fs";
|
|
1161
1191
|
import * as path3 from "path";
|
|
1162
1192
|
|
|
1163
|
-
// src/formatters/html/template.ts
|
|
1193
|
+
// src/formatters/html/template-scripts.ts
|
|
1164
1194
|
var JS_THEME = `
|
|
1165
1195
|
// Theme management
|
|
1166
1196
|
function getSystemTheme() {
|
|
@@ -1962,6 +1992,23 @@ function parseMarkdownSections(marked) {
|
|
|
1962
1992
|
});
|
|
1963
1993
|
}
|
|
1964
1994
|
`;
|
|
1995
|
+
var JS_HTML_EMBED = `
|
|
1996
|
+
// Open srcdoc-embedded HTML (doc-html iframes) in a new tab via a blob URL
|
|
1997
|
+
function initHtmlEmbeds() {
|
|
1998
|
+
document.querySelectorAll('.doc-html-open-srcdoc').forEach((btn) => {
|
|
1999
|
+
btn.addEventListener('click', function() {
|
|
2000
|
+
const container = btn.closest('.doc-html');
|
|
2001
|
+
const iframe = container ? container.querySelector('iframe.doc-html-frame') : null;
|
|
2002
|
+
const html = iframe ? iframe.getAttribute('srcdoc') : null;
|
|
2003
|
+
if (!html) return;
|
|
2004
|
+
const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
|
|
2005
|
+
window.open(url, '_blank', 'noopener');
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
`;
|
|
2010
|
+
|
|
2011
|
+
// src/formatters/html/template.ts
|
|
1965
2012
|
function generateScript(options) {
|
|
1966
2013
|
const initCalls = [];
|
|
1967
2014
|
if (options.includeDarkMode) {
|
|
@@ -1979,6 +2026,7 @@ function generateScript(options) {
|
|
|
1979
2026
|
initCalls.push("initHashScroll();");
|
|
1980
2027
|
initCalls.push("initToc();");
|
|
1981
2028
|
initCalls.push("initThemePicker();");
|
|
2029
|
+
initCalls.push("initHtmlEmbeds();");
|
|
1982
2030
|
const initScript = `
|
|
1983
2031
|
// Initialize on load
|
|
1984
2032
|
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -1987,6 +2035,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1987
2035
|
`;
|
|
1988
2036
|
let script = options.includeDarkMode ? JS_THEME : "";
|
|
1989
2037
|
script += JS_CORE;
|
|
2038
|
+
script += JS_HTML_EMBED;
|
|
1990
2039
|
if (options.additionalJs) {
|
|
1991
2040
|
script += options.additionalJs;
|
|
1992
2041
|
}
|
|
@@ -3652,6 +3701,82 @@ body {
|
|
|
3652
3701
|
opacity: 0.8;
|
|
3653
3702
|
}
|
|
3654
3703
|
|
|
3704
|
+
/* ============================================================================
|
|
3705
|
+
Documentation Entries - Embedded HTML
|
|
3706
|
+
============================================================================ */
|
|
3707
|
+
.doc-html {
|
|
3708
|
+
margin-bottom: 0.5rem;
|
|
3709
|
+
border: 1px solid var(--border);
|
|
3710
|
+
border-radius: calc(var(--radius) - 2px);
|
|
3711
|
+
overflow: hidden;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
.doc-html:last-child {
|
|
3715
|
+
margin-bottom: 0;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
.doc-html-header {
|
|
3719
|
+
display: flex;
|
|
3720
|
+
align-items: center;
|
|
3721
|
+
justify-content: space-between;
|
|
3722
|
+
gap: 0.5rem;
|
|
3723
|
+
padding: 0.375rem 0.75rem;
|
|
3724
|
+
background: var(--muted, transparent);
|
|
3725
|
+
border-bottom: 1px solid var(--border);
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
.doc-html-title {
|
|
3729
|
+
font-size: 0.75rem;
|
|
3730
|
+
font-weight: 600;
|
|
3731
|
+
color: var(--muted-foreground);
|
|
3732
|
+
text-transform: uppercase;
|
|
3733
|
+
letter-spacing: 0.04em;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
.doc-html-open {
|
|
3737
|
+
font-size: 0.875rem;
|
|
3738
|
+
line-height: 1;
|
|
3739
|
+
padding: 0.125rem 0.375rem;
|
|
3740
|
+
border: 1px solid var(--border);
|
|
3741
|
+
border-radius: calc(var(--radius) - 4px);
|
|
3742
|
+
background: transparent;
|
|
3743
|
+
color: var(--muted-foreground);
|
|
3744
|
+
cursor: pointer;
|
|
3745
|
+
text-decoration: none;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
.doc-html-open:hover {
|
|
3749
|
+
color: var(--foreground);
|
|
3750
|
+
border-color: var(--foreground);
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
.doc-html-frame {
|
|
3754
|
+
display: block;
|
|
3755
|
+
width: 100%;
|
|
3756
|
+
border: 0;
|
|
3757
|
+
background: #fff;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
.doc-html-missing {
|
|
3761
|
+
padding: 0.75rem 1rem;
|
|
3762
|
+
border: 1px dashed var(--border);
|
|
3763
|
+
background: var(--muted, transparent);
|
|
3764
|
+
color: var(--muted-foreground);
|
|
3765
|
+
font-size: 0.8125rem;
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
.doc-html-missing-label {
|
|
3769
|
+
font-weight: 600;
|
|
3770
|
+
margin-bottom: 0.25rem;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
.doc-html-missing-path {
|
|
3774
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
3775
|
+
font-size: 0.75rem;
|
|
3776
|
+
word-break: break-all;
|
|
3777
|
+
opacity: 0.8;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3655
3780
|
/* ============================================================================
|
|
3656
3781
|
Documentation Entries - Visual Check
|
|
3657
3782
|
============================================================================ */
|
|
@@ -13950,6 +14075,36 @@ function renderDocVideo(entry, deps) {
|
|
|
13950
14075
|
${captionHtml}
|
|
13951
14076
|
</div>`;
|
|
13952
14077
|
}
|
|
14078
|
+
function resolveHtmlSource(entry, deps) {
|
|
14079
|
+
if (entry.url !== void 0) return { mode: "src", value: entry.url };
|
|
14080
|
+
if (entry.content !== void 0) return { mode: "srcdoc", value: entry.content };
|
|
14081
|
+
const filePath = entry.path ?? "";
|
|
14082
|
+
if (/^https?:/i.test(filePath)) return { mode: "src", value: filePath };
|
|
14083
|
+
const inlined = deps.readHtmlFile?.(filePath);
|
|
14084
|
+
if (inlined !== void 0) return { mode: "srcdoc", value: inlined };
|
|
14085
|
+
const isAbsoluteFsPath = /^(?:[/\\]|[A-Za-z]:[/\\])/.test(filePath);
|
|
14086
|
+
if (deps.readHtmlFile && isAbsoluteFsPath) return { mode: "missing", value: filePath };
|
|
14087
|
+
return { mode: "src", value: filePath };
|
|
14088
|
+
}
|
|
14089
|
+
function renderDocHtml(entry, deps) {
|
|
14090
|
+
const source = resolveHtmlSource(entry, deps);
|
|
14091
|
+
if (source.mode === "missing") {
|
|
14092
|
+
return `<div class="doc-html doc-html-missing">
|
|
14093
|
+
<div class="doc-html-missing-label">HTML unavailable</div>
|
|
14094
|
+
<div class="doc-html-missing-path">${deps.escapeHtml(source.value)}</div>
|
|
14095
|
+
</div>`;
|
|
14096
|
+
}
|
|
14097
|
+
const heightCss = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
|
|
14098
|
+
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>`;
|
|
14099
|
+
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>`;
|
|
14100
|
+
return `<div class="doc-html">
|
|
14101
|
+
<div class="doc-html-header">
|
|
14102
|
+
<span class="doc-html-title">${deps.escapeHtml(entry.title ?? "HTML")}</span>
|
|
14103
|
+
${openBtn}
|
|
14104
|
+
</div>
|
|
14105
|
+
${frame}
|
|
14106
|
+
</div>`;
|
|
14107
|
+
}
|
|
13953
14108
|
function renderDocCustom(entry, deps) {
|
|
13954
14109
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
13955
14110
|
const data = entry.data;
|
|
@@ -14006,6 +14161,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14006
14161
|
case "video":
|
|
14007
14162
|
html = renderDocVideo(entry, deps);
|
|
14008
14163
|
break;
|
|
14164
|
+
case "html":
|
|
14165
|
+
html = renderDocHtml(entry, deps);
|
|
14166
|
+
break;
|
|
14009
14167
|
case "custom":
|
|
14010
14168
|
html = renderDocCustom(entry, deps);
|
|
14011
14169
|
break;
|
|
@@ -14577,6 +14735,21 @@ function readScreenshotAsDataUri(filePath) {
|
|
|
14577
14735
|
return void 0;
|
|
14578
14736
|
}
|
|
14579
14737
|
}
|
|
14738
|
+
var HTML_INLINE_WARN_BYTES = 1024 * 1024;
|
|
14739
|
+
function readHtmlFileContent(filePath) {
|
|
14740
|
+
try {
|
|
14741
|
+
if (!fs2.existsSync(filePath)) return void 0;
|
|
14742
|
+
const buf = fs2.readFileSync(filePath);
|
|
14743
|
+
if (buf.byteLength > HTML_INLINE_WARN_BYTES) {
|
|
14744
|
+
console.warn(
|
|
14745
|
+
`[executable-stories] Inlining large HTML file (${Math.round(buf.byteLength / 1024)} KiB) into the report: ${filePath}. Consider --asset-mode copy.`
|
|
14746
|
+
);
|
|
14747
|
+
}
|
|
14748
|
+
return buf.toString("utf8");
|
|
14749
|
+
} catch {
|
|
14750
|
+
return void 0;
|
|
14751
|
+
}
|
|
14752
|
+
}
|
|
14580
14753
|
function normalizeOptions(options = {}) {
|
|
14581
14754
|
return {
|
|
14582
14755
|
title: options.title ?? "Test Results",
|
|
@@ -14584,6 +14757,7 @@ function normalizeOptions(options = {}) {
|
|
|
14584
14757
|
searchable: options.searchable ?? true,
|
|
14585
14758
|
startCollapsed: options.startCollapsed ?? false,
|
|
14586
14759
|
embedScreenshots: options.embedScreenshots ?? true,
|
|
14760
|
+
embedHtmlFiles: options.embedHtmlFiles ?? true,
|
|
14587
14761
|
syntaxHighlighting: options.syntaxHighlighting ?? true,
|
|
14588
14762
|
mermaidEnabled: options.mermaidEnabled ?? true,
|
|
14589
14763
|
markdownEnabled: options.markdownEnabled ?? true,
|
|
@@ -14602,7 +14776,10 @@ function createHtmlFormatter(options = {}) {
|
|
|
14602
14776
|
markdownEnabled: opts.markdownEnabled,
|
|
14603
14777
|
mermaidEnabled: opts.mermaidEnabled,
|
|
14604
14778
|
embedScreenshots: opts.embedScreenshots,
|
|
14605
|
-
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath)
|
|
14779
|
+
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath),
|
|
14780
|
+
// When html-file inlining is off (e.g. --asset-mode copy), omit the read
|
|
14781
|
+
// hook so doc-html iframes keep their src path for the asset bundler.
|
|
14782
|
+
...opts.embedHtmlFiles ? { readHtmlFile: (filePath) => readHtmlFileContent(filePath) } : {}
|
|
14606
14783
|
};
|
|
14607
14784
|
const renderDocs = (docs, containerClass) => {
|
|
14608
14785
|
if (!docs || docs.length === 0) return "";
|
|
@@ -14894,6 +15071,8 @@ var JUnitFormatter = class {
|
|
|
14894
15071
|
}
|
|
14895
15072
|
case "screenshot":
|
|
14896
15073
|
return `${indent}Screenshot: ${entry.alt ?? entry.path}`;
|
|
15074
|
+
case "html":
|
|
15075
|
+
return `${indent}HTML: ${entry.title ?? "Embedded HTML"} (${entry.url ?? entry.path ?? "inline"})`;
|
|
14897
15076
|
case "custom": {
|
|
14898
15077
|
const dataStr = JSON.stringify(entry.data, null, 2);
|
|
14899
15078
|
const lines = [];
|
|
@@ -15345,6 +15524,25 @@ var MarkdownFormatter = class {
|
|
|
15345
15524
|
lines.push(`${indent}`);
|
|
15346
15525
|
break;
|
|
15347
15526
|
}
|
|
15527
|
+
case "html": {
|
|
15528
|
+
const htmlLabel = entry.title ?? "Embedded HTML";
|
|
15529
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
15530
|
+
lines.push(`${indent}[${htmlLabel}](${entry.url ?? entry.path})`);
|
|
15531
|
+
break;
|
|
15532
|
+
}
|
|
15533
|
+
lines.push(`${indent}<details>`);
|
|
15534
|
+
lines.push(`${indent}<summary>${htmlLabel}</summary>`);
|
|
15535
|
+
lines.push(`${indent}`);
|
|
15536
|
+
lines.push(`${indent}\`\`\`html`);
|
|
15537
|
+
for (const line of (entry.content ?? "").split("\n")) {
|
|
15538
|
+
lines.push(`${indent}${line}`);
|
|
15539
|
+
}
|
|
15540
|
+
lines.push(`${indent}\`\`\``);
|
|
15541
|
+
lines.push(`${indent}`);
|
|
15542
|
+
lines.push(`${indent}</details>`);
|
|
15543
|
+
lines.push(`${indent}`);
|
|
15544
|
+
break;
|
|
15545
|
+
}
|
|
15348
15546
|
case "custom":
|
|
15349
15547
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15350
15548
|
const data = entry.data;
|
|
@@ -15519,6 +15717,132 @@ function escapePipe(value) {
|
|
|
15519
15717
|
return value.replace(/\|/g, "\\|");
|
|
15520
15718
|
}
|
|
15521
15719
|
|
|
15720
|
+
// src/formatters/traceability-matrix.ts
|
|
15721
|
+
var TraceabilityMatrixFormatter = class {
|
|
15722
|
+
format(run) {
|
|
15723
|
+
const matrix = toTraceabilityMatrix(run);
|
|
15724
|
+
const lines = [];
|
|
15725
|
+
lines.push("# Traceability Matrix");
|
|
15726
|
+
lines.push("");
|
|
15727
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
15728
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
15729
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
15730
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
15731
|
+
lines.push("");
|
|
15732
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
15733
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
15734
|
+
lines.push(
|
|
15735
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
15736
|
+
);
|
|
15737
|
+
lines.push("");
|
|
15738
|
+
for (const req of matrix.requirements) {
|
|
15739
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
15740
|
+
lines.push(`## ${heading2}`);
|
|
15741
|
+
lines.push("");
|
|
15742
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
15743
|
+
if (req.covers.length > 0) {
|
|
15744
|
+
lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
|
|
15745
|
+
}
|
|
15746
|
+
lines.push("");
|
|
15747
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
15748
|
+
lines.push("| --- | --- | --- | --- |");
|
|
15749
|
+
for (const scenario of req.scenarios) {
|
|
15750
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15751
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
|
|
15752
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
15753
|
+
}
|
|
15754
|
+
lines.push("");
|
|
15755
|
+
}
|
|
15756
|
+
if (matrix.untraced.length > 0) {
|
|
15757
|
+
lines.push("## Untraced scenarios");
|
|
15758
|
+
lines.push("");
|
|
15759
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
15760
|
+
lines.push("");
|
|
15761
|
+
lines.push("| Status | Scenario | Source |");
|
|
15762
|
+
lines.push("| --- | --- | --- |");
|
|
15763
|
+
for (const scenario of matrix.untraced) {
|
|
15764
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15765
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
15766
|
+
}
|
|
15767
|
+
lines.push("");
|
|
15768
|
+
}
|
|
15769
|
+
return lines.join("\n").trimEnd();
|
|
15770
|
+
}
|
|
15771
|
+
};
|
|
15772
|
+
function toTraceabilityMatrix(run) {
|
|
15773
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
15774
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
15775
|
+
const untraced = [];
|
|
15776
|
+
for (const tc of sorted) {
|
|
15777
|
+
const tickets = tc.story.tickets ?? [];
|
|
15778
|
+
if (tickets.length === 0) {
|
|
15779
|
+
untraced.push({
|
|
15780
|
+
id: tc.id,
|
|
15781
|
+
title: tc.story.scenario,
|
|
15782
|
+
status: tc.status,
|
|
15783
|
+
sourceFile: tc.sourceFile,
|
|
15784
|
+
sourceLine: tc.sourceLine
|
|
15785
|
+
});
|
|
15786
|
+
continue;
|
|
15787
|
+
}
|
|
15788
|
+
for (const ticket of tickets) {
|
|
15789
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
15790
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
15791
|
+
entry.cases.push(tc);
|
|
15792
|
+
byTicket.set(ticket.id, entry);
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
15796
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
15797
|
+
id: tc.id,
|
|
15798
|
+
title: tc.story.scenario,
|
|
15799
|
+
status: tc.status,
|
|
15800
|
+
sourceFile: tc.sourceFile,
|
|
15801
|
+
sourceLine: tc.sourceLine,
|
|
15802
|
+
covers: tc.story.covers ?? []
|
|
15803
|
+
}));
|
|
15804
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
15805
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
15806
|
+
});
|
|
15807
|
+
return {
|
|
15808
|
+
schemaVersion: "1.0",
|
|
15809
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15810
|
+
run: {
|
|
15811
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
15812
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
15813
|
+
gitSha: run.gitSha,
|
|
15814
|
+
branch: run.ci?.branch
|
|
15815
|
+
},
|
|
15816
|
+
summary: {
|
|
15817
|
+
requirements: requirements.length,
|
|
15818
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
15819
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
15820
|
+
scenarios: run.testCases.length,
|
|
15821
|
+
untracedScenarios: untraced.length
|
|
15822
|
+
},
|
|
15823
|
+
requirements,
|
|
15824
|
+
untraced
|
|
15825
|
+
};
|
|
15826
|
+
}
|
|
15827
|
+
function requirementStatus(cases) {
|
|
15828
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
15829
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
15830
|
+
return "incomplete";
|
|
15831
|
+
}
|
|
15832
|
+
function renderRequirementStatus(status) {
|
|
15833
|
+
switch (status) {
|
|
15834
|
+
case "verified":
|
|
15835
|
+
return "verified (all scenarios passed)";
|
|
15836
|
+
case "failing":
|
|
15837
|
+
return "failing (a scenario failed)";
|
|
15838
|
+
default:
|
|
15839
|
+
return "incomplete (no scenario passed yet)";
|
|
15840
|
+
}
|
|
15841
|
+
}
|
|
15842
|
+
function escapePipe2(value) {
|
|
15843
|
+
return value.replace(/\|/g, "\\|");
|
|
15844
|
+
}
|
|
15845
|
+
|
|
15522
15846
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
15523
15847
|
function extractFeatureName(testCases, uri) {
|
|
15524
15848
|
for (const tc of testCases) {
|
|
@@ -16588,6 +16912,8 @@ function formatDocEntry(doc) {
|
|
|
16588
16912
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16589
16913
|
case "video":
|
|
16590
16914
|
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16915
|
+
case "html":
|
|
16916
|
+
return `${doc.title ? `${escapeHtml2(doc.title)}: ` : ""}${escapeHtml2(doc.url ?? doc.path ?? "(inline html)")}`;
|
|
16591
16917
|
case "custom":
|
|
16592
16918
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
16593
16919
|
}
|
|
@@ -17046,6 +17372,8 @@ function formatDocEntry2(doc) {
|
|
|
17046
17372
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17047
17373
|
case "video":
|
|
17048
17374
|
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
17375
|
+
case "html":
|
|
17376
|
+
return `${doc.title ? `${doc.title}: ` : ""}${doc.url ?? doc.path ?? "(inline html)"}`;
|
|
17049
17377
|
case "custom":
|
|
17050
17378
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17051
17379
|
}
|
|
@@ -17195,7 +17523,7 @@ import * as path5 from "path";
|
|
|
17195
17523
|
function scanHtmlAssets(html) {
|
|
17196
17524
|
const seen = /* @__PURE__ */ new Set();
|
|
17197
17525
|
const patterns = [
|
|
17198
|
-
/<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
17526
|
+
/<(?:img|video|iframe)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
17199
17527
|
/<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
|
|
17200
17528
|
/<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
|
|
17201
17529
|
];
|
|
@@ -17274,7 +17602,7 @@ function bundleAssets(htmlPath, options = {}) {
|
|
|
17274
17602
|
function replaceAssetRef(html, original, replacement) {
|
|
17275
17603
|
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17276
17604
|
const srcPattern = new RegExp(
|
|
17277
|
-
`(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
17605
|
+
`(<(?:img|video|iframe)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
17278
17606
|
"g"
|
|
17279
17607
|
);
|
|
17280
17608
|
html = html.replace(srcPattern, `$1${replacement}$2`);
|
|
@@ -17662,6 +17990,21 @@ ${tc.errorStack}` : "");
|
|
|
17662
17990
|
])
|
|
17663
17991
|
);
|
|
17664
17992
|
break;
|
|
17993
|
+
case "html":
|
|
17994
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
17995
|
+
const target = entry.url ?? entry.path ?? "";
|
|
17996
|
+
content.push(
|
|
17997
|
+
paragraph([
|
|
17998
|
+
text(entry.title ?? "Embedded HTML", strong()),
|
|
17999
|
+
text(": "),
|
|
18000
|
+
link(target, target)
|
|
18001
|
+
])
|
|
18002
|
+
);
|
|
18003
|
+
break;
|
|
18004
|
+
}
|
|
18005
|
+
content.push(paragraph([text(entry.title ?? "Embedded HTML", strong())]));
|
|
18006
|
+
content.push(codeBlock(entry.content ?? "", "html"));
|
|
18007
|
+
break;
|
|
17665
18008
|
case "custom":
|
|
17666
18009
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
17667
18010
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -19378,6 +19721,27 @@ function resolveTraceUrl(template, traceId) {
|
|
|
19378
19721
|
return template.replace(/\{traceId\}/g, traceId);
|
|
19379
19722
|
}
|
|
19380
19723
|
|
|
19724
|
+
// src/utils/doc-builders.ts
|
|
19725
|
+
function buildHtmlDocEntry(options) {
|
|
19726
|
+
const sources = [options.path, options.url, options.content].filter(
|
|
19727
|
+
(v) => v !== void 0
|
|
19728
|
+
);
|
|
19729
|
+
if (sources.length !== 1) {
|
|
19730
|
+
throw new Error(
|
|
19731
|
+
"story.html() requires exactly one of path, url, or content"
|
|
19732
|
+
);
|
|
19733
|
+
}
|
|
19734
|
+
return {
|
|
19735
|
+
kind: "html",
|
|
19736
|
+
path: options.path,
|
|
19737
|
+
url: options.url,
|
|
19738
|
+
content: options.content,
|
|
19739
|
+
title: options.title,
|
|
19740
|
+
height: options.height,
|
|
19741
|
+
phase: "runtime"
|
|
19742
|
+
};
|
|
19743
|
+
}
|
|
19744
|
+
|
|
19381
19745
|
// src/notifiers/slack.ts
|
|
19382
19746
|
function truncate(text2, maxLen) {
|
|
19383
19747
|
if (text2.length <= maxLen) return text2;
|
|
@@ -20226,6 +20590,280 @@ function collectDocKinds(testCase) {
|
|
|
20226
20590
|
return [...kinds].sort();
|
|
20227
20591
|
}
|
|
20228
20592
|
|
|
20593
|
+
// src/scenario-failure.ts
|
|
20594
|
+
function failingScenarioMessage(tc) {
|
|
20595
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20596
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20597
|
+
}
|
|
20598
|
+
|
|
20599
|
+
// src/check.ts
|
|
20600
|
+
var ICON_PASS = "\u2713";
|
|
20601
|
+
var ICON_FAIL = "\u2717";
|
|
20602
|
+
var ICON_SKIP = "\u2298";
|
|
20603
|
+
var ICON_PENDING = "\u23F3";
|
|
20604
|
+
var ICON_WARN = "\u26A0";
|
|
20605
|
+
function buildCheck(args, _deps = {}) {
|
|
20606
|
+
const { testCases, baseline } = args;
|
|
20607
|
+
const summary = {
|
|
20608
|
+
total: testCases.length,
|
|
20609
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20610
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20611
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20612
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20613
|
+
};
|
|
20614
|
+
let regressed = 0;
|
|
20615
|
+
let fixed = 0;
|
|
20616
|
+
if (baseline) {
|
|
20617
|
+
for (const tc of testCases) {
|
|
20618
|
+
const before = baseline.get(tc.id);
|
|
20619
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20620
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20621
|
+
}
|
|
20622
|
+
}
|
|
20623
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20624
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20625
|
+
return a.location.localeCompare(b.location);
|
|
20626
|
+
});
|
|
20627
|
+
return {
|
|
20628
|
+
summary,
|
|
20629
|
+
failures,
|
|
20630
|
+
regressed,
|
|
20631
|
+
fixed,
|
|
20632
|
+
comparedToBaseline: baseline !== void 0
|
|
20633
|
+
};
|
|
20634
|
+
}
|
|
20635
|
+
function toFailure(tc, baseline) {
|
|
20636
|
+
const failedIndexes = new Set(
|
|
20637
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20638
|
+
);
|
|
20639
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20640
|
+
keyword: step.keyword,
|
|
20641
|
+
text: step.text,
|
|
20642
|
+
failed: failedIndexes.has(index)
|
|
20643
|
+
}));
|
|
20644
|
+
return {
|
|
20645
|
+
id: tc.id,
|
|
20646
|
+
scenario: tc.story.scenario,
|
|
20647
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20648
|
+
steps,
|
|
20649
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20650
|
+
covers: tc.story.covers ?? [],
|
|
20651
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20652
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20653
|
+
};
|
|
20654
|
+
}
|
|
20655
|
+
function renderCheck(report, format) {
|
|
20656
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20657
|
+
}
|
|
20658
|
+
function renderCheckText(report) {
|
|
20659
|
+
const { summary, failures } = report;
|
|
20660
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20661
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20662
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20663
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20664
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20665
|
+
if (failures.length === 0) {
|
|
20666
|
+
const lines2 = [headline];
|
|
20667
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20668
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20669
|
+
}
|
|
20670
|
+
lines2.push("All scenarios green.");
|
|
20671
|
+
return lines2.join("\n");
|
|
20672
|
+
}
|
|
20673
|
+
const lines = [headline, ""];
|
|
20674
|
+
for (const f of failures) {
|
|
20675
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20676
|
+
lines.push(` ${f.location}`);
|
|
20677
|
+
for (const step of f.steps) {
|
|
20678
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20679
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20680
|
+
}
|
|
20681
|
+
if (f.errorMessage) {
|
|
20682
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20683
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20684
|
+
}
|
|
20685
|
+
if (f.covers.length > 0) {
|
|
20686
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20687
|
+
}
|
|
20688
|
+
if (f.tickets.length > 0) {
|
|
20689
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20690
|
+
}
|
|
20691
|
+
lines.push("");
|
|
20692
|
+
}
|
|
20693
|
+
if (report.comparedToBaseline) {
|
|
20694
|
+
if (report.regressed > 0) {
|
|
20695
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20696
|
+
}
|
|
20697
|
+
if (report.fixed > 0) {
|
|
20698
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20699
|
+
}
|
|
20700
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20701
|
+
lines.push("No status changes vs. baseline.");
|
|
20702
|
+
}
|
|
20703
|
+
}
|
|
20704
|
+
return lines.join("\n").trimEnd();
|
|
20705
|
+
}
|
|
20706
|
+
|
|
20707
|
+
// src/goal.ts
|
|
20708
|
+
var ACTIVE = ["passed", "failed"];
|
|
20709
|
+
function buildGoal(args, _deps = {}) {
|
|
20710
|
+
const { run, baseline } = args;
|
|
20711
|
+
const cases = run.testCases;
|
|
20712
|
+
const selectors = [
|
|
20713
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20714
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20715
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20716
|
+
];
|
|
20717
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20718
|
+
const regressions = [];
|
|
20719
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20720
|
+
const before = statusMap(baseline);
|
|
20721
|
+
for (const tc of cases) {
|
|
20722
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20723
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20724
|
+
}
|
|
20725
|
+
}
|
|
20726
|
+
}
|
|
20727
|
+
const violations = [];
|
|
20728
|
+
if (baseline && args.enforceRatchet) {
|
|
20729
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20730
|
+
for (const base of baseline.testCases) {
|
|
20731
|
+
const now = current.get(base.id);
|
|
20732
|
+
if (!now) {
|
|
20733
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20734
|
+
continue;
|
|
20735
|
+
}
|
|
20736
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20737
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20738
|
+
}
|
|
20739
|
+
const baseSteps = base.story.steps.length;
|
|
20740
|
+
const nowSteps = now.story.steps.length;
|
|
20741
|
+
if (nowSteps < baseSteps) {
|
|
20742
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20743
|
+
}
|
|
20744
|
+
}
|
|
20745
|
+
}
|
|
20746
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20747
|
+
return {
|
|
20748
|
+
met,
|
|
20749
|
+
requirements,
|
|
20750
|
+
regressions,
|
|
20751
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20752
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20753
|
+
};
|
|
20754
|
+
}
|
|
20755
|
+
function evaluate(selector, matched) {
|
|
20756
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20757
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20758
|
+
return {
|
|
20759
|
+
selector,
|
|
20760
|
+
matched: matched.length,
|
|
20761
|
+
passed,
|
|
20762
|
+
failing,
|
|
20763
|
+
met: matched.length > 0 && failing.length === 0
|
|
20764
|
+
};
|
|
20765
|
+
}
|
|
20766
|
+
function statusMap(run) {
|
|
20767
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20768
|
+
}
|
|
20769
|
+
function renderGoal(report, format) {
|
|
20770
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20771
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20772
|
+
for (const req of report.requirements) {
|
|
20773
|
+
if (req.matched === 0) {
|
|
20774
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20775
|
+
continue;
|
|
20776
|
+
}
|
|
20777
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20778
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20779
|
+
}
|
|
20780
|
+
if (report.regressionsEnforced) {
|
|
20781
|
+
if (report.regressions.length === 0) {
|
|
20782
|
+
lines.push(" regressions: 0");
|
|
20783
|
+
} else {
|
|
20784
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20785
|
+
}
|
|
20786
|
+
}
|
|
20787
|
+
if (report.ratchet.enforced) {
|
|
20788
|
+
if (report.ratchet.violations.length === 0) {
|
|
20789
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20790
|
+
} else {
|
|
20791
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20792
|
+
for (const v of report.ratchet.violations) {
|
|
20793
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20794
|
+
}
|
|
20795
|
+
}
|
|
20796
|
+
}
|
|
20797
|
+
return lines.join("\n");
|
|
20798
|
+
}
|
|
20799
|
+
|
|
20800
|
+
// src/triage.ts
|
|
20801
|
+
function buildTriage(args, _deps = {}) {
|
|
20802
|
+
const { testCases, baseline } = args;
|
|
20803
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20804
|
+
const ranked = failing.map((tc) => {
|
|
20805
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20806
|
+
return {
|
|
20807
|
+
tc,
|
|
20808
|
+
regressed,
|
|
20809
|
+
covers: tc.story.covers ?? []
|
|
20810
|
+
};
|
|
20811
|
+
}).sort((a, b) => {
|
|
20812
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20813
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20814
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20815
|
+
return la.localeCompare(lb);
|
|
20816
|
+
});
|
|
20817
|
+
const items = ranked.map((entry, index) => ({
|
|
20818
|
+
rank: index + 1,
|
|
20819
|
+
id: entry.tc.id,
|
|
20820
|
+
scenario: entry.tc.story.scenario,
|
|
20821
|
+
status: entry.tc.status,
|
|
20822
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20823
|
+
covers: entry.covers,
|
|
20824
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20825
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20826
|
+
regressed: entry.regressed,
|
|
20827
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20828
|
+
}));
|
|
20829
|
+
return {
|
|
20830
|
+
total: testCases.length,
|
|
20831
|
+
failing: failing.length,
|
|
20832
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20833
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20834
|
+
items
|
|
20835
|
+
};
|
|
20836
|
+
}
|
|
20837
|
+
function renderTriage(report, format) {
|
|
20838
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20839
|
+
if (report.items.length === 0) {
|
|
20840
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20841
|
+
}
|
|
20842
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20843
|
+
const lines = [header, ""];
|
|
20844
|
+
for (const item of report.items) {
|
|
20845
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20846
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20847
|
+
lines.push(` ${item.location}`);
|
|
20848
|
+
if (item.errorMessage) {
|
|
20849
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20850
|
+
}
|
|
20851
|
+
if (item.covers.length > 0) {
|
|
20852
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20853
|
+
} else {
|
|
20854
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
20855
|
+
}
|
|
20856
|
+
if (item.tickets.length > 0) {
|
|
20857
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
20858
|
+
}
|
|
20859
|
+
lines.push("");
|
|
20860
|
+
}
|
|
20861
|
+
if (report.needsCovers > 0) {
|
|
20862
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
20863
|
+
}
|
|
20864
|
+
return lines.join("\n").trimEnd();
|
|
20865
|
+
}
|
|
20866
|
+
|
|
20229
20867
|
// src/review/conventions.ts
|
|
20230
20868
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20231
20869
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -21027,6 +21665,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
21027
21665
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
21028
21666
|
markdown: ".md",
|
|
21029
21667
|
"release-manifest": ".release-manifest.md",
|
|
21668
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
21030
21669
|
html: ".html",
|
|
21031
21670
|
"cucumber-html": ".cucumber.html",
|
|
21032
21671
|
junit: ".junit.xml",
|
|
@@ -21187,6 +21826,9 @@ var ReportGenerator = class {
|
|
|
21187
21826
|
searchable: options.html?.searchable ?? true,
|
|
21188
21827
|
startCollapsed: options.html?.startCollapsed ?? false,
|
|
21189
21828
|
embedScreenshots: options.html?.embedScreenshots ?? true,
|
|
21829
|
+
// Under "copy" asset mode local html files become hashed assets with
|
|
21830
|
+
// an iframe src instead of being inlined into the report.
|
|
21831
|
+
embedHtmlFiles: options.html?.embedHtmlFiles ?? (options.assetMode ?? "none") !== "copy",
|
|
21190
21832
|
syntaxHighlighting: options.html?.syntaxHighlighting ?? true,
|
|
21191
21833
|
mermaidEnabled: options.html?.mermaidEnabled ?? true,
|
|
21192
21834
|
markdownEnabled: options.html?.markdownEnabled ?? true,
|
|
@@ -21362,6 +22004,7 @@ var ReportGenerator = class {
|
|
|
21362
22004
|
searchable: this.options.html.searchable,
|
|
21363
22005
|
startCollapsed: this.options.html.startCollapsed,
|
|
21364
22006
|
embedScreenshots: this.options.html.embedScreenshots,
|
|
22007
|
+
embedHtmlFiles: this.options.html.embedHtmlFiles,
|
|
21365
22008
|
syntaxHighlighting: this.options.html.syntaxHighlighting,
|
|
21366
22009
|
mermaidEnabled: this.options.html.mermaidEnabled,
|
|
21367
22010
|
markdownEnabled: this.options.html.markdownEnabled,
|
|
@@ -21449,6 +22092,10 @@ var ReportGenerator = class {
|
|
|
21449
22092
|
const formatter = new ReleaseManifestFormatter();
|
|
21450
22093
|
return formatter.format(run);
|
|
21451
22094
|
}
|
|
22095
|
+
case "traceability-matrix": {
|
|
22096
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
22097
|
+
return formatter.format(run);
|
|
22098
|
+
}
|
|
21452
22099
|
case "story-report-json": {
|
|
21453
22100
|
const formatter = new StoryReportJsonFormatter({
|
|
21454
22101
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21528,11 +22175,16 @@ export {
|
|
|
21528
22175
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21529
22176
|
ScenarioIndexJsonFormatter,
|
|
21530
22177
|
StoryReportJsonFormatter,
|
|
22178
|
+
TraceabilityMatrixFormatter,
|
|
21531
22179
|
adaptJestRun,
|
|
21532
22180
|
adaptPlaywrightRun,
|
|
21533
22181
|
adaptVitestRun,
|
|
21534
22182
|
assertValidRun,
|
|
22183
|
+
buildCheck,
|
|
22184
|
+
buildGoal,
|
|
22185
|
+
buildHtmlDocEntry,
|
|
21535
22186
|
buildReview,
|
|
22187
|
+
buildTriage,
|
|
21536
22188
|
bundleAssets,
|
|
21537
22189
|
calculateFlakiness,
|
|
21538
22190
|
calculateStability,
|
|
@@ -21582,6 +22234,9 @@ export {
|
|
|
21582
22234
|
readPackageVersion,
|
|
21583
22235
|
recordDeployment,
|
|
21584
22236
|
regenerateArtifacts,
|
|
22237
|
+
renderCheck,
|
|
22238
|
+
renderGoal,
|
|
22239
|
+
renderTriage,
|
|
21585
22240
|
resolveAttachment,
|
|
21586
22241
|
resolveAttachments,
|
|
21587
22242
|
resolveTheme,
|
|
@@ -21603,6 +22258,7 @@ export {
|
|
|
21603
22258
|
toReleaseManifest,
|
|
21604
22259
|
toScenarioIndex,
|
|
21605
22260
|
toStoryReport,
|
|
22261
|
+
toTraceabilityMatrix,
|
|
21606
22262
|
tryGetActiveOtelContext,
|
|
21607
22263
|
updateHistory,
|
|
21608
22264
|
validateCanonicalRun
|