executable-stories-formatters 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +351 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +272 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -3
- package/dist/index.d.ts +81 -3
- package/dist/index.js +268 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs7 from "fs";
|
|
6
|
+
import * as path7 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -492,17 +492,17 @@ function validateRawRun(data) {
|
|
|
492
492
|
return { valid: true, errors: [] };
|
|
493
493
|
}
|
|
494
494
|
const errors = (validate.errors ?? []).map((err) => {
|
|
495
|
-
const
|
|
495
|
+
const path8 = err.instancePath || "/";
|
|
496
496
|
const message = err.message ?? "unknown error";
|
|
497
497
|
if (err.keyword === "additionalProperties") {
|
|
498
498
|
const extra = err.params.additionalProperty;
|
|
499
|
-
return `${
|
|
499
|
+
return `${path8}: ${message} \u2014 '${extra}'`;
|
|
500
500
|
}
|
|
501
501
|
if (err.keyword === "enum") {
|
|
502
502
|
const allowed = err.params.allowedValues;
|
|
503
|
-
return `${
|
|
503
|
+
return `${path8}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
504
504
|
}
|
|
505
|
-
return `${
|
|
505
|
+
return `${path8}: ${message}`;
|
|
506
506
|
});
|
|
507
507
|
return { valid: false, errors };
|
|
508
508
|
}
|
|
@@ -966,7 +966,7 @@ ${result.errors.join("\n")}`);
|
|
|
966
966
|
|
|
967
967
|
// src/index.ts
|
|
968
968
|
import "fs";
|
|
969
|
-
import * as
|
|
969
|
+
import * as path5 from "path";
|
|
970
970
|
import * as fsPromises from "fs/promises";
|
|
971
971
|
|
|
972
972
|
// src/converters/acl/lines.ts
|
|
@@ -4028,6 +4028,12 @@ body {
|
|
|
4028
4028
|
font-weight: 600;
|
|
4029
4029
|
font-size: 0.875rem;
|
|
4030
4030
|
color: var(--foreground);
|
|
4031
|
+
text-decoration: none;
|
|
4032
|
+
cursor: pointer;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
a.toc-title:hover {
|
|
4036
|
+
color: var(--primary);
|
|
4031
4037
|
}
|
|
4032
4038
|
|
|
4033
4039
|
.toc-feature {
|
|
@@ -4205,7 +4211,7 @@ function corporateBuildBody(args, deps) {
|
|
|
4205
4211
|
const sidebar = `
|
|
4206
4212
|
<nav class="toc">
|
|
4207
4213
|
<div class="toc-header">
|
|
4208
|
-
<
|
|
4214
|
+
<a href="#" class="toc-title" onclick="window.scrollTo({top:0,behavior:'smooth'});return false;">Test Report</a>
|
|
4209
4215
|
<div class="toc-stats">
|
|
4210
4216
|
<div class="toc-stat-row">
|
|
4211
4217
|
<span class="toc-stat-label">Total</span>
|
|
@@ -13825,7 +13831,7 @@ function renderScenario(args, deps) {
|
|
|
13825
13831
|
<div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
13826
13832
|
</div>
|
|
13827
13833
|
<div class="scenario-actions">
|
|
13828
|
-
<button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"
|
|
13834
|
+
<button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
|
13829
13835
|
<button class="permalink-anchor" onclick="copyPermalink('scenario-${tc.id}')" aria-label="Copy link to scenario" title="Copy link">#</button>
|
|
13830
13836
|
<span class="scenario-duration">${duration}</span>
|
|
13831
13837
|
</div>
|
|
@@ -14204,7 +14210,7 @@ function renderToc(args, deps) {
|
|
|
14204
14210
|
}
|
|
14205
14211
|
return `<nav class="toc-sidebar" aria-label="Table of contents">
|
|
14206
14212
|
<div class="toc-header">
|
|
14207
|
-
<
|
|
14213
|
+
<a href="#" class="toc-title" onclick="window.scrollTo({top:0,behavior:'smooth'});return false;">Contents</a>
|
|
14208
14214
|
</div>
|
|
14209
14215
|
<div class="toc-body">
|
|
14210
14216
|
${features.join("\n")}
|
|
@@ -15665,8 +15671,8 @@ function extractDocAttachments(step) {
|
|
|
15665
15671
|
}
|
|
15666
15672
|
return attachments;
|
|
15667
15673
|
}
|
|
15668
|
-
function guessMediaType(
|
|
15669
|
-
const lower =
|
|
15674
|
+
function guessMediaType(path8) {
|
|
15675
|
+
const lower = path8.toLowerCase();
|
|
15670
15676
|
if (lower.endsWith(".png")) return "image/png";
|
|
15671
15677
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
15672
15678
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -15807,11 +15813,11 @@ var CucumberHtmlFormatter = class {
|
|
|
15807
15813
|
for (const envelope of envelopes) {
|
|
15808
15814
|
const accepted = htmlStream.write(envelope);
|
|
15809
15815
|
if (!accepted) {
|
|
15810
|
-
await new Promise((
|
|
15816
|
+
await new Promise((resolve7) => htmlStream.once("drain", resolve7));
|
|
15811
15817
|
}
|
|
15812
15818
|
}
|
|
15813
|
-
await new Promise((
|
|
15814
|
-
collector.on("finish",
|
|
15819
|
+
await new Promise((resolve7, reject) => {
|
|
15820
|
+
collector.on("finish", resolve7);
|
|
15815
15821
|
collector.on("error", reject);
|
|
15816
15822
|
htmlStream.end();
|
|
15817
15823
|
});
|
|
@@ -16843,6 +16849,177 @@ function replaceAssetRef(html, original, replacement) {
|
|
|
16843
16849
|
return html;
|
|
16844
16850
|
}
|
|
16845
16851
|
|
|
16852
|
+
// src/formatters/astro.ts
|
|
16853
|
+
var AstroFormatter = class _AstroFormatter {
|
|
16854
|
+
markdownFormatter;
|
|
16855
|
+
title;
|
|
16856
|
+
constructor(options = {}) {
|
|
16857
|
+
this.title = options.markdown?.title ?? "User Stories";
|
|
16858
|
+
this.markdownFormatter = new MarkdownFormatter({
|
|
16859
|
+
...options.markdown,
|
|
16860
|
+
title: this.title,
|
|
16861
|
+
stepStyle: "gherkin",
|
|
16862
|
+
includeFrontMatter: false,
|
|
16863
|
+
includeSummaryTable: false,
|
|
16864
|
+
includeMetadata: false
|
|
16865
|
+
});
|
|
16866
|
+
}
|
|
16867
|
+
format(run) {
|
|
16868
|
+
const markdown = this.markdownFormatter.format(run);
|
|
16869
|
+
const body = markdown.replace(/^# .+\n\n?/, "");
|
|
16870
|
+
const frontmatter = this.buildFrontmatter(run);
|
|
16871
|
+
return `${frontmatter}
|
|
16872
|
+
${body}`;
|
|
16873
|
+
}
|
|
16874
|
+
buildFrontmatter(run) {
|
|
16875
|
+
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
16876
|
+
const count = run.testCases.length;
|
|
16877
|
+
const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
16878
|
+
const lines = [
|
|
16879
|
+
"---",
|
|
16880
|
+
`title: ${this.title}`,
|
|
16881
|
+
`description: ${description}`,
|
|
16882
|
+
"sidebar:",
|
|
16883
|
+
" badge:",
|
|
16884
|
+
` text: ${badge.text}`,
|
|
16885
|
+
` variant: ${badge.variant}`,
|
|
16886
|
+
"---"
|
|
16887
|
+
];
|
|
16888
|
+
return lines.join("\n");
|
|
16889
|
+
}
|
|
16890
|
+
static computeBadge(testCases) {
|
|
16891
|
+
const statuses = new Set(testCases.map((tc) => tc.status));
|
|
16892
|
+
if (statuses.has("failed")) return { text: "Failed", variant: "danger" };
|
|
16893
|
+
if (statuses.has("pending")) return { text: "Pending", variant: "caution" };
|
|
16894
|
+
if (statuses.has("skipped") && !statuses.has("passed")) return { text: "Skipped", variant: "caution" };
|
|
16895
|
+
return { text: "Passed", variant: "success" };
|
|
16896
|
+
}
|
|
16897
|
+
};
|
|
16898
|
+
|
|
16899
|
+
// src/formatters/astro-assets.ts
|
|
16900
|
+
import * as fs4 from "fs";
|
|
16901
|
+
import * as path4 from "path";
|
|
16902
|
+
var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
|
|
16903
|
+
function isLocalPath(src) {
|
|
16904
|
+
const trimmed = src.trim();
|
|
16905
|
+
if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
|
|
16906
|
+
return false;
|
|
16907
|
+
}
|
|
16908
|
+
return !path4.posix.isAbsolute(trimmed) && !path4.win32.isAbsolute(trimmed);
|
|
16909
|
+
}
|
|
16910
|
+
function stripCodeContent(markdown) {
|
|
16911
|
+
let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
16912
|
+
result = result.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
16913
|
+
result = result.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, "");
|
|
16914
|
+
result = result.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, "");
|
|
16915
|
+
return result;
|
|
16916
|
+
}
|
|
16917
|
+
function scanMarkdownAssets(markdown) {
|
|
16918
|
+
const found = /* @__PURE__ */ new Set();
|
|
16919
|
+
const stripped = stripCodeContent(markdown);
|
|
16920
|
+
const mdImageRe = /!\[[^\]]*\]\(([^)"'\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
16921
|
+
let match;
|
|
16922
|
+
while ((match = mdImageRe.exec(stripped)) !== null) {
|
|
16923
|
+
const src = match[1].trim();
|
|
16924
|
+
if (isLocalPath(src)) {
|
|
16925
|
+
found.add(src);
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16928
|
+
const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
|
|
16929
|
+
while ((match = htmlSrcRe.exec(stripped)) !== null) {
|
|
16930
|
+
const src = match[1].trim();
|
|
16931
|
+
if (isLocalPath(src)) {
|
|
16932
|
+
found.add(src);
|
|
16933
|
+
}
|
|
16934
|
+
}
|
|
16935
|
+
return Array.from(found);
|
|
16936
|
+
}
|
|
16937
|
+
function splitByCode(markdown) {
|
|
16938
|
+
const codeRe = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$|<pre\b[^>]*>[\s\S]*?<\/pre>|<code\b[^>]*>[\s\S]*?<\/code>|(`+)(?:(?!\2).)+\2/gim;
|
|
16939
|
+
const segments = [];
|
|
16940
|
+
let lastIndex = 0;
|
|
16941
|
+
for (const match of markdown.matchAll(codeRe)) {
|
|
16942
|
+
if (match.index > lastIndex) {
|
|
16943
|
+
segments.push(markdown.slice(lastIndex, match.index));
|
|
16944
|
+
}
|
|
16945
|
+
segments.push(match[0]);
|
|
16946
|
+
lastIndex = match.index + match[0].length;
|
|
16947
|
+
}
|
|
16948
|
+
if (lastIndex < markdown.length) {
|
|
16949
|
+
segments.push(markdown.slice(lastIndex));
|
|
16950
|
+
}
|
|
16951
|
+
return segments;
|
|
16952
|
+
}
|
|
16953
|
+
function isCode(segment) {
|
|
16954
|
+
const trimmed = segment.trimStart();
|
|
16955
|
+
return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
|
|
16956
|
+
}
|
|
16957
|
+
function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
16958
|
+
let result = prose;
|
|
16959
|
+
result = result.replace(
|
|
16960
|
+
/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
|
|
16961
|
+
(full, pre, src, post) => {
|
|
16962
|
+
const trimmed = src.trim();
|
|
16963
|
+
if (!isLocalPath(trimmed)) return full;
|
|
16964
|
+
if (pathMap) {
|
|
16965
|
+
const mapped = pathMap.get(trimmed);
|
|
16966
|
+
if (mapped === void 0) return full;
|
|
16967
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
16968
|
+
}
|
|
16969
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
16970
|
+
}
|
|
16971
|
+
);
|
|
16972
|
+
result = result.replace(
|
|
16973
|
+
/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
|
|
16974
|
+
(full, pre, src, post) => {
|
|
16975
|
+
const trimmed = src.trim();
|
|
16976
|
+
if (!isLocalPath(trimmed)) return full;
|
|
16977
|
+
if (pathMap) {
|
|
16978
|
+
const mapped = pathMap.get(trimmed);
|
|
16979
|
+
if (mapped === void 0) return full;
|
|
16980
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
16981
|
+
}
|
|
16982
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
16983
|
+
}
|
|
16984
|
+
);
|
|
16985
|
+
return result;
|
|
16986
|
+
}
|
|
16987
|
+
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
16988
|
+
return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
|
|
16989
|
+
}
|
|
16990
|
+
function copyMarkdownAssets(options) {
|
|
16991
|
+
const {
|
|
16992
|
+
markdown,
|
|
16993
|
+
markdownDir,
|
|
16994
|
+
assetsDir,
|
|
16995
|
+
assetsBaseUrl,
|
|
16996
|
+
allowMissing = false
|
|
16997
|
+
} = options;
|
|
16998
|
+
const refs = scanMarkdownAssets(markdown);
|
|
16999
|
+
const pathMap = /* @__PURE__ */ new Map();
|
|
17000
|
+
const missing = [];
|
|
17001
|
+
for (const ref of refs) {
|
|
17002
|
+
const absPath = path4.resolve(markdownDir, ref);
|
|
17003
|
+
if (!fs4.existsSync(absPath)) {
|
|
17004
|
+
if (!allowMissing) {
|
|
17005
|
+
throw new Error(`Asset not found: ${absPath}`);
|
|
17006
|
+
}
|
|
17007
|
+
missing.push(ref);
|
|
17008
|
+
continue;
|
|
17009
|
+
}
|
|
17010
|
+
const relativeCopied = copyAsset(absPath, assetsDir);
|
|
17011
|
+
const fileName = relativeCopied.replace(/^assets\//, "");
|
|
17012
|
+
pathMap.set(ref, fileName);
|
|
17013
|
+
}
|
|
17014
|
+
const rewritten = rewriteAssetPaths(markdown, assetsBaseUrl, pathMap);
|
|
17015
|
+
return {
|
|
17016
|
+
markdown: rewritten,
|
|
17017
|
+
copiedCount: pathMap.size,
|
|
17018
|
+
missingCount: missing.length,
|
|
17019
|
+
missing
|
|
17020
|
+
};
|
|
17021
|
+
}
|
|
17022
|
+
|
|
16846
17023
|
// src/converters/ndjson-parser.ts
|
|
16847
17024
|
function parseNdjson(ndjson) {
|
|
16848
17025
|
const lines = ndjson.trim().split("\n").filter(Boolean);
|
|
@@ -17837,6 +18014,7 @@ function listScenarios(args, _deps) {
|
|
|
17837
18014
|
|
|
17838
18015
|
// src/index.ts
|
|
17839
18016
|
var FORMAT_EXTENSIONS = {
|
|
18017
|
+
astro: ".md",
|
|
17840
18018
|
markdown: ".md",
|
|
17841
18019
|
html: ".html",
|
|
17842
18020
|
"cucumber-html": ".cucumber.html",
|
|
@@ -17869,11 +18047,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
17869
18047
|
const ext = FORMAT_EXTENSIONS[format];
|
|
17870
18048
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
17871
18049
|
if (mode === "aggregated") {
|
|
17872
|
-
return toPosix(
|
|
18050
|
+
return toPosix(path5.join(baseOutputDir, `${effectiveName}${ext}`));
|
|
17873
18051
|
}
|
|
17874
18052
|
const normalizedSource = toPosix(sourceFile);
|
|
17875
|
-
const dirOfSource =
|
|
17876
|
-
let baseName =
|
|
18053
|
+
const dirOfSource = path5.posix.dirname(normalizedSource);
|
|
18054
|
+
let baseName = path5.posix.basename(normalizedSource);
|
|
17877
18055
|
for (const testExt of TEST_EXTENSIONS) {
|
|
17878
18056
|
if (baseName.endsWith(testExt)) {
|
|
17879
18057
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -17882,9 +18060,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
17882
18060
|
}
|
|
17883
18061
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
17884
18062
|
if (colocatedStyle === "adjacent") {
|
|
17885
|
-
return toPosix(
|
|
18063
|
+
return toPosix(path5.posix.join(dirOfSource, fileName));
|
|
17886
18064
|
}
|
|
17887
|
-
return toPosix(
|
|
18065
|
+
return toPosix(path5.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
17888
18066
|
}
|
|
17889
18067
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
17890
18068
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -17955,7 +18133,7 @@ var ReportGenerator = class {
|
|
|
17955
18133
|
excludeTags: options.excludeTags ?? [],
|
|
17956
18134
|
formats: options.formats ?? ["cucumber-json"],
|
|
17957
18135
|
outputDir: options.outputDir ?? "reports",
|
|
17958
|
-
outputName: options.outputName ?? "
|
|
18136
|
+
outputName: options.outputName ?? "index",
|
|
17959
18137
|
outputNameTimestamp: options.outputNameTimestamp ?? false,
|
|
17960
18138
|
sortTestCases: options.sortTestCases ?? "none",
|
|
17961
18139
|
output: {
|
|
@@ -18010,6 +18188,24 @@ var ReportGenerator = class {
|
|
|
18010
18188
|
includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
|
|
18011
18189
|
customRenderers: options.markdown?.customRenderers
|
|
18012
18190
|
},
|
|
18191
|
+
astro: {
|
|
18192
|
+
assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
|
|
18193
|
+
assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
|
|
18194
|
+
markdown: {
|
|
18195
|
+
title: options.astro?.markdown?.title ?? "User Stories",
|
|
18196
|
+
includeStatusIcons: options.astro?.markdown?.includeStatusIcons ?? true,
|
|
18197
|
+
includeErrors: options.astro?.markdown?.includeErrors ?? true,
|
|
18198
|
+
scenarioHeadingLevel: options.astro?.markdown?.scenarioHeadingLevel ?? 3,
|
|
18199
|
+
groupBy: options.astro?.markdown?.groupBy ?? "file",
|
|
18200
|
+
sortScenarios: options.astro?.markdown?.sortScenarios ?? "source",
|
|
18201
|
+
suiteSeparator: options.astro?.markdown?.suiteSeparator ?? " - ",
|
|
18202
|
+
includeSourceLinks: options.astro?.markdown?.includeSourceLinks ?? true,
|
|
18203
|
+
permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
|
|
18204
|
+
ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
|
|
18205
|
+
traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
|
|
18206
|
+
customRenderers: options.astro?.markdown?.customRenderers
|
|
18207
|
+
}
|
|
18208
|
+
},
|
|
18013
18209
|
assetMode: options.assetMode ?? "none",
|
|
18014
18210
|
allowMissingAssets: options.allowMissingAssets ?? false
|
|
18015
18211
|
};
|
|
@@ -18047,6 +18243,24 @@ var ReportGenerator = class {
|
|
|
18047
18243
|
});
|
|
18048
18244
|
}
|
|
18049
18245
|
}
|
|
18246
|
+
const astroPaths = results.get("astro");
|
|
18247
|
+
if (astroPaths) {
|
|
18248
|
+
for (const mdPath of astroPaths) {
|
|
18249
|
+
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
18250
|
+
const mdDir = path5.dirname(mdPath);
|
|
18251
|
+
const assetsDir = path5.resolve(this.options.astro.assetsDir);
|
|
18252
|
+
const result = copyMarkdownAssets({
|
|
18253
|
+
markdown: content,
|
|
18254
|
+
markdownDir: mdDir,
|
|
18255
|
+
assetsDir,
|
|
18256
|
+
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
18257
|
+
allowMissing: this.options.allowMissingAssets
|
|
18258
|
+
});
|
|
18259
|
+
if (result.copiedCount > 0 || result.missingCount > 0) {
|
|
18260
|
+
await this.deps.writeFile(mdPath, result.markdown);
|
|
18261
|
+
}
|
|
18262
|
+
}
|
|
18263
|
+
}
|
|
18050
18264
|
}
|
|
18051
18265
|
return results;
|
|
18052
18266
|
}
|
|
@@ -18065,9 +18279,9 @@ var ReportGenerator = class {
|
|
|
18065
18279
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
18066
18280
|
const ext = FORMAT_EXTENSIONS[format];
|
|
18067
18281
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
18068
|
-
const outputPath = toPosix(
|
|
18282
|
+
const outputPath = toPosix(path5.join(this.options.outputDir, `${effectiveName}${ext}`));
|
|
18069
18283
|
const content = await this.formatContent(run, format);
|
|
18070
|
-
const dir =
|
|
18284
|
+
const dir = path5.dirname(outputPath);
|
|
18071
18285
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
18072
18286
|
await this.deps.writeFile(outputPath, content);
|
|
18073
18287
|
return [outputPath];
|
|
@@ -18079,7 +18293,7 @@ var ReportGenerator = class {
|
|
|
18079
18293
|
testCases
|
|
18080
18294
|
};
|
|
18081
18295
|
const content = await this.formatContent(groupRun, format);
|
|
18082
|
-
const dir =
|
|
18296
|
+
const dir = path5.dirname(outputPath);
|
|
18083
18297
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
18084
18298
|
await this.deps.writeFile(outputPath, content);
|
|
18085
18299
|
writtenPaths.push(outputPath);
|
|
@@ -18142,6 +18356,13 @@ var ReportGenerator = class {
|
|
|
18142
18356
|
});
|
|
18143
18357
|
return formatter.formatToString(run);
|
|
18144
18358
|
}
|
|
18359
|
+
case "astro": {
|
|
18360
|
+
const formatter = new AstroFormatter({
|
|
18361
|
+
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
18362
|
+
markdown: this.options.astro.markdown
|
|
18363
|
+
});
|
|
18364
|
+
return formatter.format(run);
|
|
18365
|
+
}
|
|
18145
18366
|
case "markdown": {
|
|
18146
18367
|
const formatter = new MarkdownFormatter({
|
|
18147
18368
|
title: this.options.markdown.title,
|
|
@@ -18176,7 +18397,7 @@ async function generateRunComparison(args) {
|
|
|
18176
18397
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
18177
18398
|
for (const format of args.formats) {
|
|
18178
18399
|
const ext = format === "html" ? ".html" : ".md";
|
|
18179
|
-
const outputPath = toPosix(
|
|
18400
|
+
const outputPath = toPosix(path5.join(outputDir, `${outputName}${ext}`));
|
|
18180
18401
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
18181
18402
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
18182
18403
|
files.push(outputPath);
|
|
@@ -18184,6 +18405,45 @@ async function generateRunComparison(args) {
|
|
|
18184
18405
|
return { files, diff };
|
|
18185
18406
|
}
|
|
18186
18407
|
|
|
18408
|
+
// src/init-astro.ts
|
|
18409
|
+
import * as fs6 from "fs";
|
|
18410
|
+
import * as path6 from "path";
|
|
18411
|
+
import { fileURLToPath } from "url";
|
|
18412
|
+
var __dirname = path6.dirname(fileURLToPath(import.meta.url));
|
|
18413
|
+
function initAstro(options = {}) {
|
|
18414
|
+
const targetDir = options.targetDir ?? "./story-docs";
|
|
18415
|
+
const force = options.force ?? false;
|
|
18416
|
+
if (fs6.existsSync(targetDir)) {
|
|
18417
|
+
const entries = fs6.readdirSync(targetDir);
|
|
18418
|
+
if (entries.length > 0 && !force) {
|
|
18419
|
+
throw new Error(
|
|
18420
|
+
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
|
|
18421
|
+
);
|
|
18422
|
+
}
|
|
18423
|
+
}
|
|
18424
|
+
const templateDir = path6.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
18425
|
+
if (!fs6.existsSync(templateDir)) {
|
|
18426
|
+
throw new Error(
|
|
18427
|
+
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
18428
|
+
);
|
|
18429
|
+
}
|
|
18430
|
+
copyDirRecursive(templateDir, targetDir);
|
|
18431
|
+
return { targetDir };
|
|
18432
|
+
}
|
|
18433
|
+
function copyDirRecursive(src, dest) {
|
|
18434
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
18435
|
+
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
18436
|
+
for (const entry of entries) {
|
|
18437
|
+
const srcPath = path6.join(src, entry.name);
|
|
18438
|
+
const destPath = path6.join(dest, entry.name);
|
|
18439
|
+
if (entry.isDirectory()) {
|
|
18440
|
+
copyDirRecursive(srcPath, destPath);
|
|
18441
|
+
} else {
|
|
18442
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
18443
|
+
}
|
|
18444
|
+
}
|
|
18445
|
+
}
|
|
18446
|
+
|
|
18187
18447
|
// src/cli.ts
|
|
18188
18448
|
var EXIT_SUCCESS = 0;
|
|
18189
18449
|
var EXIT_SCHEMA_VALIDATION = 1;
|
|
@@ -18200,15 +18460,18 @@ USAGE
|
|
|
18200
18460
|
executable-stories list <file> [options]
|
|
18201
18461
|
executable-stories validate <file>
|
|
18202
18462
|
executable-stories validate --stdin
|
|
18463
|
+
executable-stories init-astro [directory]
|
|
18203
18464
|
|
|
18204
18465
|
SUBCOMMANDS
|
|
18205
18466
|
format Read raw test results and generate reports
|
|
18206
18467
|
compare Compare two runs and generate a diff report
|
|
18207
18468
|
list List scenarios from a test run (text table or JSON)
|
|
18208
18469
|
validate Validate a JSON file against the schema (no output generated)
|
|
18470
|
+
init-astro Scaffold an Astro Starlight docs site for story output
|
|
18209
18471
|
|
|
18210
18472
|
OPTIONS
|
|
18211
18473
|
--format <formats> Comma-separated formats (default: html)
|
|
18474
|
+
astro Starlight-compatible Markdown (for Astro docs sites)
|
|
18212
18475
|
html Custom HTML report (accessible, dark mode, mermaid)
|
|
18213
18476
|
cucumber-html Official Cucumber HTML report
|
|
18214
18477
|
markdown Markdown documentation
|
|
@@ -18217,7 +18480,7 @@ OPTIONS
|
|
|
18217
18480
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
18218
18481
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
18219
18482
|
--output-dir <dir> Output directory (default: reports)
|
|
18220
|
-
--output-name <name> Base filename (default:
|
|
18483
|
+
--output-name <name> Base filename (default: index)
|
|
18221
18484
|
--output-name-timestamp Append run timestamp (UTC seconds) to output filename for before/after diffs
|
|
18222
18485
|
--sort-test-cases <mode> Sort scenarios deterministically: id, source, none (default: none)
|
|
18223
18486
|
--include <globs> Comma-separated globs to include test cases by sourceFile (e.g. "**/*.Story*.cs")
|
|
@@ -18255,6 +18518,10 @@ COMPARE
|
|
|
18255
18518
|
compare supports --format html,markdown
|
|
18256
18519
|
compare uses the same --input-type for both baseline and current files
|
|
18257
18520
|
|
|
18521
|
+
INIT-ASTRO
|
|
18522
|
+
executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
|
|
18523
|
+
--force Overwrite existing directory
|
|
18524
|
+
|
|
18258
18525
|
NOTIFICATIONS
|
|
18259
18526
|
--slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
|
|
18260
18527
|
--teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
|
|
@@ -18289,10 +18556,31 @@ function parseCliArgs(argv) {
|
|
|
18289
18556
|
process.exit(EXIT_SUCCESS);
|
|
18290
18557
|
}
|
|
18291
18558
|
const subcommand = args[0];
|
|
18292
|
-
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate") {
|
|
18293
|
-
console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", or "
|
|
18559
|
+
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro") {
|
|
18560
|
+
console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", or "init-astro".`);
|
|
18294
18561
|
process.exit(EXIT_USAGE);
|
|
18295
18562
|
}
|
|
18563
|
+
if (subcommand === "init-astro") {
|
|
18564
|
+
const initArgs = args.slice(1);
|
|
18565
|
+
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
18566
|
+
const force = initArgs.includes("--force");
|
|
18567
|
+
try {
|
|
18568
|
+
const result = initAstro({ targetDir, force });
|
|
18569
|
+
console.log(`Scaffolded Astro Starlight project at ${result.targetDir}`);
|
|
18570
|
+
console.log("");
|
|
18571
|
+
console.log("Next steps:");
|
|
18572
|
+
console.log(` cd ${result.targetDir}`);
|
|
18573
|
+
console.log(" pnpm install # or npm install");
|
|
18574
|
+
console.log(" pnpm dev # start the dev server");
|
|
18575
|
+
console.log("");
|
|
18576
|
+
console.log("Generate story docs with:");
|
|
18577
|
+
console.log(` executable-stories format run.json --format astro --output-dir ${result.targetDir}/src/content/docs/stories --asset-mode copy`);
|
|
18578
|
+
process.exit(EXIT_SUCCESS);
|
|
18579
|
+
} catch (err) {
|
|
18580
|
+
console.error(`Error: ${err.message}`);
|
|
18581
|
+
process.exit(EXIT_USAGE);
|
|
18582
|
+
}
|
|
18583
|
+
}
|
|
18296
18584
|
const { values, positionals } = parseArgs({
|
|
18297
18585
|
args: args.slice(1),
|
|
18298
18586
|
options: {
|
|
@@ -18301,7 +18589,7 @@ function parseCliArgs(argv) {
|
|
|
18301
18589
|
"baseline-dir": { type: "string" },
|
|
18302
18590
|
"input-type": { type: "string", default: "raw" },
|
|
18303
18591
|
"output-dir": { type: "string", default: "reports" },
|
|
18304
|
-
"output-name": { type: "string", default: "
|
|
18592
|
+
"output-name": { type: "string", default: "index" },
|
|
18305
18593
|
"output-name-timestamp": { type: "boolean", default: false },
|
|
18306
18594
|
"sort-test-cases": { type: "string", default: "none" },
|
|
18307
18595
|
include: { type: "string" },
|
|
@@ -18376,12 +18664,12 @@ function parseCliArgs(argv) {
|
|
|
18376
18664
|
console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
|
|
18377
18665
|
process.exit(EXIT_USAGE);
|
|
18378
18666
|
}
|
|
18379
|
-
const validFormats = /* @__PURE__ */ new Set(["html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
|
|
18667
|
+
const validFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
|
|
18380
18668
|
const formatStr = values.format;
|
|
18381
18669
|
const formats = formatStr.split(",").map((f) => f.trim());
|
|
18382
18670
|
for (const f of formats) {
|
|
18383
18671
|
if (!validFormats.has(f)) {
|
|
18384
|
-
console.error(`Error: Unknown format "${f}". Valid: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
|
|
18672
|
+
console.error(`Error: Unknown format "${f}". Valid: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
|
|
18385
18673
|
process.exit(EXIT_USAGE);
|
|
18386
18674
|
}
|
|
18387
18675
|
}
|
|
@@ -18505,27 +18793,27 @@ async function readInput(args) {
|
|
|
18505
18793
|
if (args.stdin) {
|
|
18506
18794
|
return readStdin();
|
|
18507
18795
|
}
|
|
18508
|
-
const filePath =
|
|
18509
|
-
if (!
|
|
18796
|
+
const filePath = path7.resolve(args.inputFile);
|
|
18797
|
+
if (!fs7.existsSync(filePath)) {
|
|
18510
18798
|
console.error(`Error: File not found: ${filePath}`);
|
|
18511
18799
|
process.exit(EXIT_USAGE);
|
|
18512
18800
|
}
|
|
18513
|
-
return
|
|
18801
|
+
return fs7.readFileSync(filePath, "utf8");
|
|
18514
18802
|
}
|
|
18515
18803
|
function readFileInput(filePath) {
|
|
18516
|
-
const resolved =
|
|
18517
|
-
if (!
|
|
18804
|
+
const resolved = path7.resolve(filePath);
|
|
18805
|
+
if (!fs7.existsSync(resolved)) {
|
|
18518
18806
|
console.error(`Error: File not found: ${resolved}`);
|
|
18519
18807
|
process.exit(EXIT_USAGE);
|
|
18520
18808
|
}
|
|
18521
|
-
return
|
|
18809
|
+
return fs7.readFileSync(resolved, "utf8");
|
|
18522
18810
|
}
|
|
18523
18811
|
function readStdin() {
|
|
18524
|
-
return new Promise((
|
|
18812
|
+
return new Promise((resolve7, reject) => {
|
|
18525
18813
|
const chunks = [];
|
|
18526
18814
|
process.stdin.setEncoding("utf8");
|
|
18527
18815
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
18528
|
-
process.stdin.on("end", () =>
|
|
18816
|
+
process.stdin.on("end", () => resolve7(chunks.join("")));
|
|
18529
18817
|
process.stdin.on("error", reject);
|
|
18530
18818
|
});
|
|
18531
18819
|
}
|
|
@@ -18651,14 +18939,14 @@ function tryNormalizeRunFromText(text, args) {
|
|
|
18651
18939
|
}
|
|
18652
18940
|
}
|
|
18653
18941
|
function listBaselineCandidates(currentFile, args) {
|
|
18654
|
-
const baselineDir =
|
|
18655
|
-
const currentResolved =
|
|
18656
|
-
if (!
|
|
18942
|
+
const baselineDir = path7.resolve(args.baselineDir ?? path7.dirname(currentFile));
|
|
18943
|
+
const currentResolved = path7.resolve(currentFile);
|
|
18944
|
+
if (!fs7.existsSync(baselineDir)) {
|
|
18657
18945
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
18658
18946
|
process.exit(EXIT_USAGE);
|
|
18659
18947
|
}
|
|
18660
|
-
const entries =
|
|
18661
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
18948
|
+
const entries = fs7.readdirSync(baselineDir, { withFileTypes: true });
|
|
18949
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path7.join(baselineDir, entry.name)).filter((candidate) => path7.resolve(candidate) !== currentResolved).filter(
|
|
18662
18950
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
18663
18951
|
);
|
|
18664
18952
|
}
|
|
@@ -18666,14 +18954,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
18666
18954
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
18667
18955
|
const comparable = [];
|
|
18668
18956
|
for (const candidate of candidates) {
|
|
18669
|
-
const run = tryNormalizeRunFromText(
|
|
18957
|
+
const run = tryNormalizeRunFromText(fs7.readFileSync(candidate, "utf8"), args);
|
|
18670
18958
|
if (run) {
|
|
18671
18959
|
comparable.push({ file: candidate, run });
|
|
18672
18960
|
}
|
|
18673
18961
|
}
|
|
18674
18962
|
if (comparable.length === 0) {
|
|
18675
18963
|
console.error(
|
|
18676
|
-
`Error: no compatible baseline files found in ${
|
|
18964
|
+
`Error: no compatible baseline files found in ${path7.resolve(args.baselineDir ?? path7.dirname(currentFile))}.`
|
|
18677
18965
|
);
|
|
18678
18966
|
process.exit(EXIT_USAGE);
|
|
18679
18967
|
}
|
|
@@ -18754,9 +19042,9 @@ async function main() {
|
|
|
18754
19042
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
18755
19043
|
}
|
|
18756
19044
|
if (args.emitCanonical) {
|
|
18757
|
-
const outPath =
|
|
18758
|
-
|
|
18759
|
-
|
|
19045
|
+
const outPath = path7.resolve(args.emitCanonical);
|
|
19046
|
+
fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
|
|
19047
|
+
fs7.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
18760
19048
|
}
|
|
18761
19049
|
try {
|
|
18762
19050
|
const result = await generateReports(run, args);
|
|
@@ -18812,9 +19100,9 @@ ${msg}`);
|
|
|
18812
19100
|
}
|
|
18813
19101
|
const run = data;
|
|
18814
19102
|
if (args.emitCanonical) {
|
|
18815
|
-
const outPath =
|
|
18816
|
-
|
|
18817
|
-
|
|
19103
|
+
const outPath = path7.resolve(args.emitCanonical);
|
|
19104
|
+
fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
|
|
19105
|
+
fs7.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
18818
19106
|
}
|
|
18819
19107
|
try {
|
|
18820
19108
|
const result = await generateReports(run, args);
|
|
@@ -18869,9 +19157,9 @@ ${msg}`);
|
|
|
18869
19157
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
18870
19158
|
}
|
|
18871
19159
|
if (args.emitCanonical) {
|
|
18872
|
-
const outPath =
|
|
18873
|
-
|
|
18874
|
-
|
|
19160
|
+
const outPath = path7.resolve(args.emitCanonical);
|
|
19161
|
+
fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
|
|
19162
|
+
fs7.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
18875
19163
|
}
|
|
18876
19164
|
try {
|
|
18877
19165
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -18928,13 +19216,13 @@ async function dispatchNotifications(run, args) {
|
|
|
18928
19216
|
}
|
|
18929
19217
|
function runHistoryPipeline(run, args) {
|
|
18930
19218
|
if (!args.historyFile) return;
|
|
18931
|
-
const historyPath =
|
|
19219
|
+
const historyPath = path7.resolve(args.historyFile);
|
|
18932
19220
|
const store = loadHistory(
|
|
18933
19221
|
{ filePath: historyPath },
|
|
18934
19222
|
{
|
|
18935
19223
|
readFile: (p) => {
|
|
18936
19224
|
try {
|
|
18937
|
-
return
|
|
19225
|
+
return fs7.readFileSync(p, "utf8");
|
|
18938
19226
|
} catch {
|
|
18939
19227
|
return void 0;
|
|
18940
19228
|
}
|
|
@@ -18947,11 +19235,11 @@ function runHistoryPipeline(run, args) {
|
|
|
18947
19235
|
run,
|
|
18948
19236
|
maxRuns: args.maxHistoryRuns
|
|
18949
19237
|
});
|
|
18950
|
-
const dir =
|
|
18951
|
-
|
|
19238
|
+
const dir = path7.dirname(historyPath);
|
|
19239
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
18952
19240
|
saveHistory(
|
|
18953
19241
|
{ filePath: historyPath, store: updated },
|
|
18954
|
-
{ writeFile: (p, content) =>
|
|
19242
|
+
{ writeFile: (p, content) => fs7.writeFileSync(p, content, "utf8") }
|
|
18955
19243
|
);
|
|
18956
19244
|
let metricsCount = 0;
|
|
18957
19245
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -19049,9 +19337,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
19049
19337
|
function printCompareResult(result, args, startMs) {
|
|
19050
19338
|
const durationMs = Date.now() - startMs;
|
|
19051
19339
|
if (result.prSummary && args.prSummaryFile) {
|
|
19052
|
-
const outputPath =
|
|
19053
|
-
|
|
19054
|
-
|
|
19340
|
+
const outputPath = path7.resolve(args.prSummaryFile);
|
|
19341
|
+
fs7.mkdirSync(path7.dirname(outputPath), { recursive: true });
|
|
19342
|
+
fs7.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
19055
19343
|
}
|
|
19056
19344
|
if (args.jsonSummary) {
|
|
19057
19345
|
console.log(
|