executable-stories-formatters 0.10.0 → 0.11.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 +35 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1093 -104
- package/dist/cli.js.map +1 -1
- package/dist/{index-CbWFyoTx.d.cts → index-DF16Xl5i.d.cts} +7 -0
- package/dist/{index-CbWFyoTx.d.ts → index-DF16Xl5i.d.ts} +7 -0
- package/dist/index.cjs +130 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -6
- package/dist/index.d.ts +42 -6
- package/dist/index.js +129 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/schemas/raw-run.schema.json +19 -0
- package/schemas/story-report-v1.json +17 -0
|
@@ -113,6 +113,13 @@ type DocEntry = {
|
|
|
113
113
|
alt?: string;
|
|
114
114
|
phase: DocPhase;
|
|
115
115
|
children?: DocEntry[];
|
|
116
|
+
} | {
|
|
117
|
+
kind: "video";
|
|
118
|
+
path: string;
|
|
119
|
+
caption?: string;
|
|
120
|
+
poster?: string;
|
|
121
|
+
phase: DocPhase;
|
|
122
|
+
children?: DocEntry[];
|
|
116
123
|
} | {
|
|
117
124
|
kind: "custom";
|
|
118
125
|
type: string;
|
|
@@ -113,6 +113,13 @@ type DocEntry = {
|
|
|
113
113
|
alt?: string;
|
|
114
114
|
phase: DocPhase;
|
|
115
115
|
children?: DocEntry[];
|
|
116
|
+
} | {
|
|
117
|
+
kind: "video";
|
|
118
|
+
path: string;
|
|
119
|
+
caption?: string;
|
|
120
|
+
poster?: string;
|
|
121
|
+
phase: DocPhase;
|
|
122
|
+
children?: DocEntry[];
|
|
116
123
|
} | {
|
|
117
124
|
kind: "custom";
|
|
118
125
|
type: string;
|
package/dist/index.cjs
CHANGED
|
@@ -87,6 +87,7 @@ __export(src_exports, {
|
|
|
87
87
|
hasSufficientHistory: () => hasSufficientHistory,
|
|
88
88
|
isReviewableSource: () => isReviewableSource,
|
|
89
89
|
isTestFile: () => isTestFile,
|
|
90
|
+
joinNameAndExt: () => joinNameAndExt,
|
|
90
91
|
listScenarios: () => listScenarios,
|
|
91
92
|
loadHistory: () => loadHistory,
|
|
92
93
|
mergeStepResults: () => mergeStepResults,
|
|
@@ -893,6 +894,15 @@ function copyDocEntry(entry) {
|
|
|
893
894
|
phase: entry.phase,
|
|
894
895
|
...children
|
|
895
896
|
};
|
|
897
|
+
case "video":
|
|
898
|
+
return {
|
|
899
|
+
kind: "video",
|
|
900
|
+
path: entry.path,
|
|
901
|
+
...entry.caption ? { caption: entry.caption } : {},
|
|
902
|
+
...entry.poster ? { poster: entry.poster } : {},
|
|
903
|
+
phase: entry.phase,
|
|
904
|
+
...children
|
|
905
|
+
};
|
|
896
906
|
case "custom":
|
|
897
907
|
return {
|
|
898
908
|
kind: "custom",
|
|
@@ -14054,6 +14064,23 @@ function renderDocScreenshot(entry, deps) {
|
|
|
14054
14064
|
${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
|
|
14055
14065
|
</div>`;
|
|
14056
14066
|
}
|
|
14067
|
+
function renderDocVideo(entry, deps) {
|
|
14068
|
+
const isRemote = /^(?:https?:|data:)/i.test(entry.path);
|
|
14069
|
+
const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
14070
|
+
const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
|
|
14071
|
+
if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
|
|
14072
|
+
return `<div class="doc-video doc-video-missing">
|
|
14073
|
+
<div class="doc-video-missing-label">Video unavailable</div>
|
|
14074
|
+
<div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
|
|
14075
|
+
${captionHtml}
|
|
14076
|
+
</div>`;
|
|
14077
|
+
}
|
|
14078
|
+
const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
|
|
14079
|
+
return `<div class="doc-video">
|
|
14080
|
+
<video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
|
|
14081
|
+
${captionHtml}
|
|
14082
|
+
</div>`;
|
|
14083
|
+
}
|
|
14057
14084
|
function renderDocCustom(entry, deps) {
|
|
14058
14085
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
14059
14086
|
const data = entry.data;
|
|
@@ -14107,6 +14134,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14107
14134
|
case "screenshot":
|
|
14108
14135
|
html = renderDocScreenshot(entry, deps);
|
|
14109
14136
|
break;
|
|
14137
|
+
case "video":
|
|
14138
|
+
html = renderDocVideo(entry, deps);
|
|
14139
|
+
break;
|
|
14110
14140
|
case "custom":
|
|
14111
14141
|
html = renderDocCustom(entry, deps);
|
|
14112
14142
|
break;
|
|
@@ -15433,6 +15463,19 @@ var MarkdownFormatter = class {
|
|
|
15433
15463
|
case "screenshot":
|
|
15434
15464
|
lines.push(`${indent}`);
|
|
15435
15465
|
break;
|
|
15466
|
+
case "video": {
|
|
15467
|
+
const poster = entry.poster ? ` poster="${entry.poster}"` : "";
|
|
15468
|
+
lines.push(`${indent}`);
|
|
15469
|
+
lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
|
|
15470
|
+
lines.push(`${indent} <source src="${entry.path}" />`);
|
|
15471
|
+
lines.push(`${indent}</video>`);
|
|
15472
|
+
if (entry.caption) {
|
|
15473
|
+
lines.push(`${indent}`);
|
|
15474
|
+
lines.push(`${indent}*${entry.caption}*`);
|
|
15475
|
+
}
|
|
15476
|
+
lines.push(`${indent}`);
|
|
15477
|
+
break;
|
|
15478
|
+
}
|
|
15436
15479
|
case "custom":
|
|
15437
15480
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15438
15481
|
const data = entry.data;
|
|
@@ -16592,6 +16635,8 @@ function formatDocEntry(doc) {
|
|
|
16592
16635
|
return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
|
|
16593
16636
|
case "screenshot":
|
|
16594
16637
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16638
|
+
case "video":
|
|
16639
|
+
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
16595
16640
|
case "custom":
|
|
16596
16641
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
16597
16642
|
}
|
|
@@ -17048,6 +17093,8 @@ function formatDocEntry2(doc) {
|
|
|
17048
17093
|
return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
|
|
17049
17094
|
case "screenshot":
|
|
17050
17095
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17096
|
+
case "video":
|
|
17097
|
+
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
17051
17098
|
case "custom":
|
|
17052
17099
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17053
17100
|
}
|
|
@@ -17293,19 +17340,35 @@ function replaceAssetRef(html, original, replacement) {
|
|
|
17293
17340
|
return html;
|
|
17294
17341
|
}
|
|
17295
17342
|
|
|
17343
|
+
// src/utils/source-file.ts
|
|
17344
|
+
function cleanTestStem(fileName) {
|
|
17345
|
+
const base = fileName.split(/[\\/]/).pop() ?? fileName;
|
|
17346
|
+
const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
|
|
17347
|
+
if (stripped !== base) return stripped;
|
|
17348
|
+
return base.replace(/\.[^.]+$/, "");
|
|
17349
|
+
}
|
|
17350
|
+
function humanizeSourceFile(fileName) {
|
|
17351
|
+
return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
17352
|
+
}
|
|
17353
|
+
|
|
17296
17354
|
// src/formatters/astro.ts
|
|
17297
17355
|
var AstroFormatter = class _AstroFormatter {
|
|
17298
17356
|
markdownFormatter;
|
|
17299
17357
|
title;
|
|
17358
|
+
perFileTitle;
|
|
17300
17359
|
constructor(options = {}) {
|
|
17301
17360
|
this.title = options.markdown?.title ?? "User Stories";
|
|
17361
|
+
this.perFileTitle = options.perFileTitle ?? false;
|
|
17302
17362
|
this.markdownFormatter = new MarkdownFormatter({
|
|
17303
17363
|
...options.markdown,
|
|
17304
17364
|
title: this.title,
|
|
17305
17365
|
stepStyle: "gherkin",
|
|
17306
17366
|
includeFrontMatter: false,
|
|
17307
17367
|
includeSummaryTable: false,
|
|
17308
|
-
includeMetadata: false
|
|
17368
|
+
includeMetadata: false,
|
|
17369
|
+
// A per-file page is one file already — group by suite/describe so the
|
|
17370
|
+
// body shows clean section headings, not the redundant source path.
|
|
17371
|
+
groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
|
|
17309
17372
|
});
|
|
17310
17373
|
}
|
|
17311
17374
|
format(run) {
|
|
@@ -17315,13 +17378,31 @@ var AstroFormatter = class _AstroFormatter {
|
|
|
17315
17378
|
return `${frontmatter}
|
|
17316
17379
|
${body}`;
|
|
17317
17380
|
}
|
|
17381
|
+
/**
|
|
17382
|
+
* Title for the page. A per-file page (one source file — i.e. colocated mode)
|
|
17383
|
+
* is titled by its suite/describe name, falling back to a humanized filename,
|
|
17384
|
+
* so the docs nav reads "Convert Currency" not "User Stories" six times over.
|
|
17385
|
+
* Multi-file (aggregated) pages keep the configured title.
|
|
17386
|
+
*/
|
|
17387
|
+
deriveTitle(run) {
|
|
17388
|
+
if (!this.perFileTitle) return this.title;
|
|
17389
|
+
const sourceFiles = new Set(
|
|
17390
|
+
run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
|
|
17391
|
+
);
|
|
17392
|
+
if (sourceFiles.size !== 1) return this.title;
|
|
17393
|
+
const suites = new Set(
|
|
17394
|
+
run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
|
|
17395
|
+
);
|
|
17396
|
+
if (suites.size === 1) return [...suites][0];
|
|
17397
|
+
return humanizeSourceFile([...sourceFiles][0]) || this.title;
|
|
17398
|
+
}
|
|
17318
17399
|
buildFrontmatter(run) {
|
|
17319
17400
|
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
17320
17401
|
const count = run.testCases.length;
|
|
17321
17402
|
const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
17322
17403
|
const lines = [
|
|
17323
17404
|
"---",
|
|
17324
|
-
`title: ${this.
|
|
17405
|
+
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
17325
17406
|
`description: ${description}`,
|
|
17326
17407
|
"sidebar:",
|
|
17327
17408
|
" badge:",
|
|
@@ -17339,6 +17420,12 @@ ${body}`;
|
|
|
17339
17420
|
return { text: "Passed", variant: "success" };
|
|
17340
17421
|
}
|
|
17341
17422
|
};
|
|
17423
|
+
function yamlScalar(value) {
|
|
17424
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
17425
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
17426
|
+
}
|
|
17427
|
+
return value;
|
|
17428
|
+
}
|
|
17342
17429
|
|
|
17343
17430
|
// src/formatters/confluence.ts
|
|
17344
17431
|
var ConfluenceFormatter = class {
|
|
@@ -17615,6 +17702,15 @@ ${tc.errorStack}` : "");
|
|
|
17615
17702
|
])
|
|
17616
17703
|
);
|
|
17617
17704
|
break;
|
|
17705
|
+
case "video":
|
|
17706
|
+
content.push(
|
|
17707
|
+
paragraph([
|
|
17708
|
+
text(entry.caption ?? "Video", strong()),
|
|
17709
|
+
text(": "),
|
|
17710
|
+
link(entry.path, entry.path)
|
|
17711
|
+
])
|
|
17712
|
+
);
|
|
17713
|
+
break;
|
|
17618
17714
|
case "custom":
|
|
17619
17715
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
17620
17716
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -17784,6 +17880,13 @@ function scanMarkdownAssets(markdown) {
|
|
|
17784
17880
|
found.add(src);
|
|
17785
17881
|
}
|
|
17786
17882
|
}
|
|
17883
|
+
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
17884
|
+
while ((match = posterRe.exec(stripped)) !== null) {
|
|
17885
|
+
const src = match[1].trim();
|
|
17886
|
+
if (isLocalPath(src)) {
|
|
17887
|
+
found.add(src);
|
|
17888
|
+
}
|
|
17889
|
+
}
|
|
17787
17890
|
return Array.from(found);
|
|
17788
17891
|
}
|
|
17789
17892
|
function splitByCode(markdown) {
|
|
@@ -17834,6 +17937,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
|
17834
17937
|
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
17835
17938
|
}
|
|
17836
17939
|
);
|
|
17940
|
+
result = result.replace(
|
|
17941
|
+
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
17942
|
+
(full, pre, src, post) => {
|
|
17943
|
+
const trimmed = src.trim();
|
|
17944
|
+
if (!isLocalPath(trimmed)) return full;
|
|
17945
|
+
if (pathMap) {
|
|
17946
|
+
const mapped = pathMap.get(trimmed);
|
|
17947
|
+
if (mapped === void 0) return full;
|
|
17948
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
17949
|
+
}
|
|
17950
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
17951
|
+
}
|
|
17952
|
+
);
|
|
17837
17953
|
return result;
|
|
17838
17954
|
}
|
|
17839
17955
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
@@ -20865,6 +20981,10 @@ var FORMAT_EXTENSIONS = {
|
|
|
20865
20981
|
"scenario-index-json": ".scenarios-index.json",
|
|
20866
20982
|
"story-report-json": ".story-report.json"
|
|
20867
20983
|
};
|
|
20984
|
+
function joinNameAndExt(name, ext) {
|
|
20985
|
+
const stutter = `.${name}.`;
|
|
20986
|
+
return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
|
|
20987
|
+
}
|
|
20868
20988
|
var TEST_EXTENSIONS = [
|
|
20869
20989
|
".test.ts",
|
|
20870
20990
|
".test.tsx",
|
|
@@ -20890,7 +21010,7 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20890
21010
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20891
21011
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
20892
21012
|
if (mode === "aggregated") {
|
|
20893
|
-
return toPosix(path10.join(baseOutputDir,
|
|
21013
|
+
return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
20894
21014
|
}
|
|
20895
21015
|
const normalizedSource = toPosix(sourceFile);
|
|
20896
21016
|
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
@@ -20905,6 +21025,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20905
21025
|
if (colocatedStyle === "adjacent") {
|
|
20906
21026
|
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
20907
21027
|
}
|
|
21028
|
+
if (colocatedStyle === "flat") {
|
|
21029
|
+
return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
21030
|
+
}
|
|
20908
21031
|
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
20909
21032
|
}
|
|
20910
21033
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
@@ -21144,7 +21267,7 @@ var ReportGenerator = class {
|
|
|
21144
21267
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
21145
21268
|
const ext = FORMAT_EXTENSIONS[format];
|
|
21146
21269
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
21147
|
-
const outputPath = toPosix(path10.join(this.options.outputDir,
|
|
21270
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
21148
21271
|
const content = await this.formatContent(run, format);
|
|
21149
21272
|
const dir = path10.dirname(outputPath);
|
|
21150
21273
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
@@ -21224,6 +21347,8 @@ var ReportGenerator = class {
|
|
|
21224
21347
|
case "astro": {
|
|
21225
21348
|
const formatter = new AstroFormatter({
|
|
21226
21349
|
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
21350
|
+
// Colocated = one page per file, so title each by its own suite/file.
|
|
21351
|
+
perFileTitle: this.options.output.mode === "colocated",
|
|
21227
21352
|
markdown: this.options.astro.markdown
|
|
21228
21353
|
});
|
|
21229
21354
|
return formatter.format(run);
|
|
@@ -21377,6 +21502,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
21377
21502
|
hasSufficientHistory,
|
|
21378
21503
|
isReviewableSource,
|
|
21379
21504
|
isTestFile,
|
|
21505
|
+
joinNameAndExt,
|
|
21380
21506
|
listScenarios,
|
|
21381
21507
|
loadHistory,
|
|
21382
21508
|
mergeStepResults,
|