@anselmdk/feature-spec-md 0.6.0 → 0.6.2-rc.1
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 +35 -141
- package/dist/cli.js.map +1 -1
- package/dist/githubActionDiffReport.d.ts.map +1 -1
- package/dist/githubActionDiffReport.js +81 -15
- package/dist/githubActionDiffReport.js.map +1 -1
- package/dist/githubActionReport.d.ts.map +1 -1
- package/dist/githubActionReport.js +27 -22
- package/dist/githubActionReport.js.map +1 -1
- package/dist/githubReportMetadata.d.ts +7 -0
- package/dist/githubReportMetadata.d.ts.map +1 -0
- package/dist/githubReportMetadata.js +152 -0
- package/dist/githubReportMetadata.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mockReports.d.ts.map +1 -1
- package/dist/mockReports.js +30 -0
- package/dist/mockReports.js.map +1 -1
- package/dist/playwright.d.ts +3 -3
- package/dist/playwright.d.ts.map +1 -1
- package/dist/playwright.js +62 -15
- package/dist/playwright.js.map +1 -1
- package/dist/reportArtifacts.d.ts +7 -0
- package/dist/reportArtifacts.d.ts.map +1 -0
- package/dist/reportArtifacts.js +26 -0
- package/dist/reportArtifacts.js.map +1 -0
- package/dist/reportHtml.d.ts +11 -0
- package/dist/reportHtml.d.ts.map +1 -0
- package/dist/reportHtml.js +33 -0
- package/dist/reportHtml.js.map +1 -0
- package/dist/reportTemplate.d.ts +0 -1
- package/dist/reportTemplate.d.ts.map +1 -1
- package/dist/reportTemplate.js +146 -192
- package/dist/reportTemplate.js.map +1 -1
- package/dist/reportTemplates/shared.d.ts +6 -0
- package/dist/reportTemplates/shared.d.ts.map +1 -0
- package/dist/reportTemplates/shared.js +15 -0
- package/dist/reportTemplates/shared.js.map +1 -0
- package/dist/screenshots.d.ts +1 -3
- package/dist/screenshots.d.ts.map +1 -1
- package/dist/screenshots.js +20 -23
- package/dist/screenshots.js.map +1 -1
- package/dist/types.d.ts +3 -23
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/mocks/current/screenshots/screenshots.json +19 -1
- package/src/mocks/previous/screenshots/screenshots.json +10 -1
package/dist/playwright.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const publishedSpecRoot = "__feature-spec-md";
|
|
2
|
+
export type PublishedSpecManifestItem = {
|
|
3
|
+
filePath: string;
|
|
4
|
+
publishedPath: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function writePublishedFeatureSpecFiles(reportDir: string, patterns: string[]): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=reportArtifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportArtifacts.d.ts","sourceRoot":"","sources":["../src/reportArtifacts.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAAE,iBAuBnB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { expandFilePatterns } from "./filePatterns.js";
|
|
4
|
+
export const publishedSpecRoot = "__feature-spec-md";
|
|
5
|
+
export async function writePublishedFeatureSpecFiles(reportDir, patterns) {
|
|
6
|
+
const files = (await expandFilePatterns(patterns)).filter((file) => file.endsWith(".feature.md"));
|
|
7
|
+
const features = [];
|
|
8
|
+
for (const sourcePath of files) {
|
|
9
|
+
const filePath = safeRelativePath(sourcePath);
|
|
10
|
+
const publishedPath = `${publishedSpecRoot}/${filePath}`;
|
|
11
|
+
const target = join(reportDir, publishedPath);
|
|
12
|
+
await mkdir(dirname(target), { recursive: true });
|
|
13
|
+
await writeFile(target, await readFile(sourcePath, "utf8"), "utf8");
|
|
14
|
+
features.push({ filePath, publishedPath });
|
|
15
|
+
}
|
|
16
|
+
const manifestPath = join(reportDir, "__feature-spec-md", "manifest.json");
|
|
17
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
18
|
+
await writeFile(manifestPath, JSON.stringify({ generatedAt: new Date().toISOString(), features }, null, 2), "utf8");
|
|
19
|
+
}
|
|
20
|
+
function safeRelativePath(filePath) {
|
|
21
|
+
return filePath
|
|
22
|
+
.split(/[\\/]+/)
|
|
23
|
+
.filter((part) => part && part !== "." && part !== "..")
|
|
24
|
+
.join("/");
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=reportArtifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportArtifacts.js","sourceRoot":"","sources":["../src/reportArtifacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAOrD,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,SAAiB,EACjB,QAAkB;IAElB,MAAM,KAAK,GAAG,CAAC,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACjE,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAC7B,CAAC;IACF,MAAM,QAAQ,GAAgC,EAAE,CAAC;IAEjD,KAAK,MAAM,UAAU,IAAI,KAAK,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAC9C,MAAM,aAAa,GAAG,GAAG,iBAAiB,IAAI,QAAQ,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAC9C,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,mBAAmB,EAAE,eAAe,CAAC,CAAC;IAC3E,MAAM,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,SAAS,CACb,YAAY,EACZ,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAC5E,MAAM,CACP,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,QAAgB;IACxC,OAAO,QAAQ;SACZ,KAAK,CAAC,QAAQ,CAAC;SACf,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC;SACvD,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReportMetadataItem } from "./reportMetadata.js";
|
|
2
|
+
export type HtmlPageOptions = {
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
styles?: string;
|
|
6
|
+
scripts?: string;
|
|
7
|
+
maxWidth?: string;
|
|
8
|
+
metadata?: ReportMetadataItem[];
|
|
9
|
+
};
|
|
10
|
+
export declare function renderHtmlPage({ title, body, styles, scripts, maxWidth, metadata, }: HtmlPageOptions): string;
|
|
11
|
+
//# sourceMappingURL=reportHtml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportHtml.d.ts","sourceRoot":"","sources":["../src/reportHtml.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;CACjC,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,IAAI,EACJ,MAAW,EACX,OAAY,EACZ,QAAQ,EACR,QAAwC,GACzC,EAAE,eAAe,UA4BjB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { html } from "./html.js";
|
|
2
|
+
import { githubReportMetadataFromEnv } from "./githubReportMetadata.js";
|
|
3
|
+
import { insertReportMetadata, } from "./reportMetadata.js";
|
|
4
|
+
import { indentTemplateBlock, reportPageDefaults } from "./reportTemplates/shared.js";
|
|
5
|
+
export function renderHtmlPage({ title, body, styles = "", scripts = "", maxWidth, metadata = githubReportMetadataFromEnv(), }) {
|
|
6
|
+
const defaults = reportPageDefaults(maxWidth, styles);
|
|
7
|
+
const page = `<!doctype html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8">
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12
|
+
<title>${html(title)}</title>
|
|
13
|
+
<style>
|
|
14
|
+
body {
|
|
15
|
+
font-family: system-ui, sans-serif;
|
|
16
|
+
max-width: ${defaults.maxWidth};
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
padding: 40px 24px;
|
|
19
|
+
color: #1f2328;
|
|
20
|
+
line-height: 1.5;
|
|
21
|
+
}
|
|
22
|
+
${indentTemplateBlock(defaults.styleText, 4)}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
${indentTemplateBlock(body, 2)}
|
|
27
|
+
${scripts ? indentTemplateBlock(scripts, 2) : ""}
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
`;
|
|
31
|
+
return insertReportMetadata(page, metadata);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=reportHtml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportHtml.js","sourceRoot":"","sources":["../src/reportHtml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EACL,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAWtF,MAAM,UAAU,cAAc,CAAC,EAC7B,KAAK,EACL,IAAI,EACJ,MAAM,GAAG,EAAE,EACX,OAAO,GAAG,EAAE,EACZ,QAAQ,EACR,QAAQ,GAAG,2BAA2B,EAAE,GACxB;IAChB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG;;;;;WAKJ,IAAI,CAAC,KAAK,CAAC;;;;mBAIH,QAAQ,CAAC,QAAQ;;;;;;EAMlC,mBAAmB,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;;;;EAI1C,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC;EAC5B,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;;;CAG/C,CAAC;IAEA,OAAO,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/reportTemplate.d.ts
CHANGED
|
@@ -12,6 +12,5 @@ export type ReportOptions = {
|
|
|
12
12
|
githubRef?: string;
|
|
13
13
|
repositoryUrl?: string;
|
|
14
14
|
};
|
|
15
|
-
/** Render a complete self-contained feature spec report as HTML. */
|
|
16
15
|
export declare function renderHtmlReport(specs: FeatureSpec[], options?: ReportOptions): string;
|
|
17
16
|
//# sourceMappingURL=reportTemplate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reportTemplate.d.ts","sourceRoot":"","sources":["../src/reportTemplate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reportTemplate.d.ts","sourceRoot":"","sources":["../src/reportTemplate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,eAAe,EACf,UAAU,EACV,WAAW,EAEX,SAAS,EACT,cAAc,EACd,SAAS,EAET,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAYF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,OAAO,GAAE,aAAkB,UAcjF"}
|
package/dist/reportTemplate.js
CHANGED
|
@@ -1,79 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTML report renderer for feature specs, models, coverage state, validation
|
|
3
|
-
* issues, and optional screenshot evidence.
|
|
4
|
-
*/
|
|
5
1
|
import { html } from "./html.js";
|
|
2
|
+
import { renderHtmlPage } from "./reportHtml.js";
|
|
6
3
|
import { screenshotKey } from "./screenshots.js";
|
|
7
|
-
/** Render a complete self-contained feature spec report as HTML. */
|
|
8
4
|
export function renderHtmlReport(specs, options = {}) {
|
|
9
5
|
const title = options.title ?? "Feature Spec Report";
|
|
10
|
-
const
|
|
11
|
-
const screenshots = options.screenshots ?? [];
|
|
6
|
+
const evidence = options.screenshots ?? [];
|
|
12
7
|
const sourceLinks = {
|
|
13
8
|
githubBaseUrl: options.githubBaseUrl,
|
|
14
9
|
githubRef: options.githubRef,
|
|
15
10
|
};
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}, true);
|
|
74
|
-
</script>
|
|
75
|
-
</body>
|
|
76
|
-
</html>`;
|
|
11
|
+
return renderHtmlPage({
|
|
12
|
+
title,
|
|
13
|
+
styles: featureReportStyles(),
|
|
14
|
+
scripts: featureReportScripts(),
|
|
15
|
+
body: featureReportBody({ specs, options, evidence, sourceLinks, title }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function featureReportBody({ specs, options, evidence, sourceLinks, title, }) {
|
|
19
|
+
return `
|
|
20
|
+
<h1>${renderReportTitle(title, options.repositoryUrl)}</h1>
|
|
21
|
+
<p>Generated ${html(formatGeneratedAt(options.generatedAt))}.</p>
|
|
22
|
+
${renderIssues(options.validationIssues ?? [])}
|
|
23
|
+
${renderModels(options.models ?? [], options.coverage, sourceLinks)}
|
|
24
|
+
${specs.map((spec) => renderSpec(spec, options.coverage, evidence, sourceLinks)).join("\n")}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
function featureReportStyles() {
|
|
28
|
+
return `.panel{border:1px solid #d0d7de;border-radius:8px;padding:20px;margin:18px 0}
|
|
29
|
+
.ok{color:#1a7f37}.missing,.error{color:#cf222e}.warning{color:#9a6700}.muted{color:#57606a}
|
|
30
|
+
.badge{border:1px solid #d0d7de;border-radius:999px;padding:2px 8px;font-size:12px;white-space:nowrap}
|
|
31
|
+
.feature-header{display:flex;gap:12px;align-items:center;justify-content:space-between}
|
|
32
|
+
.scenario{border:1px solid #d0d7de;border-radius:8px;margin:12px 0;background:#fff}
|
|
33
|
+
.scenario summary{cursor:pointer;padding:14px 16px;font-weight:600}
|
|
34
|
+
.scenario-body{padding:0 16px 16px}
|
|
35
|
+
.model-item{border:1px solid #d0d7de;border-radius:8px;margin:12px 0;background:#fff}
|
|
36
|
+
.model-item summary{cursor:pointer;padding:14px 16px;font-weight:600}
|
|
37
|
+
.model-item-body{padding:0 16px 16px}.model-item-body p{margin:8px 0}
|
|
38
|
+
.table-wrap{overflow-x:auto;margin:12px 0}table{border-collapse:collapse;width:100%;font-size:14px}
|
|
39
|
+
th,td{border:1px solid #d0d7de;padding:6px 8px;text-align:left;vertical-align:top}th{background:#f6f8fa}
|
|
40
|
+
h1 a{color:#0969da;text-decoration:underline;text-underline-offset:3px}h1 a:hover{text-decoration-thickness:2px}
|
|
41
|
+
.step{border-left:3px solid #d0d7de;margin:12px 0;padding:2px 0 2px 12px}.step p{margin:8px 0}
|
|
42
|
+
.screenshots{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin:10px 0 14px}
|
|
43
|
+
.screenshot{border:1px solid #d0d7de;border-radius:8px;overflow:hidden;background:#f6f8fa}
|
|
44
|
+
.screenshot img{display:block;width:100%;height:auto}.screenshot figcaption{font-size:12px;padding:8px;color:#57606a}
|
|
45
|
+
.coverage-refs{display:inline-flex;gap:2px;margin-left:4px}.coverage-ref{color:inherit;text-decoration:underline;text-underline-offset:2px}
|
|
46
|
+
.line-link{color:inherit;text-decoration:underline;text-underline-offset:2px}`;
|
|
47
|
+
}
|
|
48
|
+
function featureReportScripts() {
|
|
49
|
+
const openTag = "<" + "script>";
|
|
50
|
+
const closeTag = "<" + "/script>";
|
|
51
|
+
return `${openTag}
|
|
52
|
+
document.addEventListener("toggle", (event) => {
|
|
53
|
+
const target = event.target;
|
|
54
|
+
if (!(target instanceof HTMLDetailsElement)) return;
|
|
55
|
+
if (!target.open || target.dataset.hasImages !== "true") return;
|
|
56
|
+
const topBefore = target.getBoundingClientRect().top;
|
|
57
|
+
document
|
|
58
|
+
.querySelectorAll('details.scenario[data-has-images="true"][open]')
|
|
59
|
+
.forEach((details) => {
|
|
60
|
+
if (details !== target) details.removeAttribute("open");
|
|
61
|
+
});
|
|
62
|
+
requestAnimationFrame(() => {
|
|
63
|
+
const topAfter = target.getBoundingClientRect().top;
|
|
64
|
+
window.scrollBy(0, topAfter - topBefore);
|
|
65
|
+
});
|
|
66
|
+
}, true);
|
|
67
|
+
${closeTag}`;
|
|
77
68
|
}
|
|
78
69
|
function renderReportTitle(title, repositoryUrl) {
|
|
79
70
|
if (!repositoryUrl)
|
|
@@ -90,20 +81,7 @@ function formatGeneratedAt(value) {
|
|
|
90
81
|
if (Number.isNaN(date.getTime()))
|
|
91
82
|
return value ?? "";
|
|
92
83
|
const day = date.getDate();
|
|
93
|
-
const month = [
|
|
94
|
-
"January",
|
|
95
|
-
"February",
|
|
96
|
-
"March",
|
|
97
|
-
"April",
|
|
98
|
-
"May",
|
|
99
|
-
"June",
|
|
100
|
-
"July",
|
|
101
|
-
"August",
|
|
102
|
-
"September",
|
|
103
|
-
"October",
|
|
104
|
-
"November",
|
|
105
|
-
"December",
|
|
106
|
-
][date.getMonth()];
|
|
84
|
+
const month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][date.getMonth()];
|
|
107
85
|
const hours = String(date.getHours()).padStart(2, "0");
|
|
108
86
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
109
87
|
return `${day}${ordinalSuffix(day)} ${month} ${date.getFullYear()} at ${hours}:${minutes}`;
|
|
@@ -135,37 +113,41 @@ function renderModels(models, coverage, sourceLinks = {}) {
|
|
|
135
113
|
const ruleScenarioLinks = buildRuleScenarioLinks(ruleCoverage, scenarioCoverage);
|
|
136
114
|
return `<section class="panel">
|
|
137
115
|
<h2>Models</h2>
|
|
138
|
-
${models
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
116
|
+
${models.map((model) => renderModel(model, modelCoverage, ruleCoverage, ruleScenarioLinks, sourceLinks)).join("\n")}
|
|
117
|
+
</section>`;
|
|
118
|
+
}
|
|
119
|
+
function renderModel(model, modelCoverage, ruleCoverage, ruleScenarioLinks, sourceLinks) {
|
|
120
|
+
return `<section>
|
|
121
|
+
<div class="feature-header">
|
|
122
|
+
<h3>${html(model.frontmatter.id)} ${html(model.title)}</h3>
|
|
123
|
+
<span class="badge">${html(model.frontmatter.status ?? "draft")}</span>
|
|
124
|
+
</div>
|
|
125
|
+
<p>${html(model.purpose)}</p>
|
|
126
|
+
<h4>Model</h4>
|
|
127
|
+
${model.modelItems
|
|
147
128
|
.map((item) => {
|
|
148
129
|
const coverageItem = modelCoverage.find((candidate) => candidate.id === item.id);
|
|
149
130
|
return `<details class="model-item">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
131
|
+
<summary><code>${html(item.id)}</code>: ${html(item.title)} ${coverageBadge(coverageItem?.covered, [], coverageItem, sourceLinks)}</summary>
|
|
132
|
+
<div class="model-item-body">${renderModelItemBody(item.body)}</div>
|
|
133
|
+
</details>`;
|
|
153
134
|
})
|
|
154
135
|
.join("")}
|
|
155
|
-
|
|
156
|
-
? `<h4>Rules</h4><ul>${model.rules
|
|
157
|
-
.map((rule) => {
|
|
158
|
-
const item = ruleCoverage.find((coverageItem) => coverageItem.id === rule.id);
|
|
159
|
-
return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks), sourceLinks)}</li>`;
|
|
160
|
-
})
|
|
161
|
-
.join("")}</ul>`
|
|
162
|
-
: ""}
|
|
163
|
-
</section>`)
|
|
164
|
-
.join("\n")}
|
|
136
|
+
${renderModelRules(model, ruleCoverage, ruleScenarioLinks, sourceLinks)}
|
|
165
137
|
</section>`;
|
|
166
138
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
139
|
+
function renderModelRules(model, ruleCoverage, ruleScenarioLinks, sourceLinks) {
|
|
140
|
+
if (!model.rules.length)
|
|
141
|
+
return "";
|
|
142
|
+
return `<h4>Rules</h4><ul>${model.rules
|
|
143
|
+
.map((rule) => {
|
|
144
|
+
const item = ruleCoverage.find((coverageItem) => coverageItem.id === rule.id);
|
|
145
|
+
return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks), sourceLinks)}</li>`;
|
|
146
|
+
})
|
|
147
|
+
.join("")}</ul>`;
|
|
148
|
+
}
|
|
149
|
+
function renderSpec(spec, coverage, evidence = [], sourceLinks = {}) {
|
|
150
|
+
const evidenceByLine = groupEvidenceByLine(evidence);
|
|
169
151
|
const ruleCoverage = coverage?.ruleCoverage ?? [];
|
|
170
152
|
const scenarioCoverage = coverage?.scenarioCoverage ?? [];
|
|
171
153
|
const ruleScenarioLinks = buildRuleScenarioLinks(ruleCoverage, scenarioCoverage);
|
|
@@ -176,35 +158,38 @@ function renderSpec(spec, coverage, screenshots = [], sourceLinks = {}) {
|
|
|
176
158
|
</div>
|
|
177
159
|
<p>${html(spec.purpose)}</p>
|
|
178
160
|
<h3>Rules</h3>
|
|
179
|
-
<ul>${spec.rules
|
|
180
|
-
.map((rule) => {
|
|
181
|
-
const item = ruleCoverage.find((coverageItem) => coverageItem.id === rule.id);
|
|
182
|
-
return `<li><code>${html(rule.id)}</code>: ${html(rule.text)} ${ruleCoverageBadge(item, ruleScenarioIds(rule.id, ruleScenarioLinks), sourceLinks)}</li>`;
|
|
183
|
-
})
|
|
184
|
-
.join("")}</ul>
|
|
161
|
+
<ul>${spec.rules.map((rule) => renderFeatureRule(rule.id, rule.text, ruleCoverage, ruleScenarioLinks, sourceLinks)).join("")}</ul>
|
|
185
162
|
<h3>Scenarios</h3>
|
|
186
|
-
${spec.scenarios
|
|
187
|
-
.map((scenario) => {
|
|
188
|
-
const screenshotCount = scenario.steps.reduce((count, step) => count +
|
|
189
|
-
(screenshotsByLine.get(screenshotKey(spec.filePath, step.line))
|
|
190
|
-
?.length ?? 0), 0);
|
|
191
|
-
const scenarioRuleIds = ruleIdsForScenario(scenario.id, spec.rules.map((rule) => rule.id), ruleScenarioLinks);
|
|
192
|
-
const scenarioCoverageItem = scenarioCoverage.find((item) => item.id === scenario.id);
|
|
193
|
-
return `<details class="scenario" data-has-images="${screenshotCount > 0 ? "true" : "false"}">
|
|
194
|
-
<summary><code>${html(scenario.id)}</code>: ${html(scenario.title)} ${coverageBadge(scenarioCoverageItem?.covered, [], scenarioCoverageItem, sourceLinks)} <span class="badge">${screenshotCount} screenshot${screenshotCount === 1 ? "" : "s"}</span></summary>
|
|
195
|
-
<div class="scenario-body">${renderScenarioRuleCoverage(scenarioRuleIds)}${scenario.steps.map((step) => renderStep(spec, step, screenshotsByLine, sourceLinks)).join("")}</div>
|
|
196
|
-
</details>`;
|
|
197
|
-
})
|
|
198
|
-
.join("\n")}
|
|
163
|
+
${spec.scenarios.map((scenario) => renderScenario(spec, scenario, scenarioCoverage, ruleScenarioLinks, evidenceByLine, sourceLinks)).join("\n")}
|
|
199
164
|
</section>`;
|
|
200
165
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
166
|
+
function renderFeatureRule(id, text, ruleCoverage, ruleScenarioLinks, sourceLinks) {
|
|
167
|
+
const item = ruleCoverage.find((coverageItem) => coverageItem.id === id);
|
|
168
|
+
return `<li><code>${html(id)}</code>: ${html(text)} ${ruleCoverageBadge(item, ruleScenarioIds(id, ruleScenarioLinks), sourceLinks)}</li>`;
|
|
169
|
+
}
|
|
170
|
+
function renderScenario(spec, scenario, scenarioCoverage, ruleScenarioLinks, evidenceByLine, sourceLinks) {
|
|
171
|
+
const scenarioEvidence = scenario.steps.flatMap((step) => evidenceByLine.get(screenshotKey(spec.filePath, step.line)) ?? []);
|
|
172
|
+
const changedCount = scenarioEvidence.filter((entry) => entry.changed && entry.path).length;
|
|
173
|
+
const unchangedCount = scenarioEvidence.filter((entry) => !entry.changed).length;
|
|
174
|
+
const scenarioRuleIds = ruleIdsForScenario(scenario.id, spec.rules.map((rule) => rule.id), ruleScenarioLinks);
|
|
175
|
+
const scenarioCoverageItem = scenarioCoverage.find((item) => item.id === scenario.id);
|
|
176
|
+
return `<details class="scenario" data-has-images="${changedCount > 0 ? "true" : "false"}">
|
|
177
|
+
<summary><code>${html(scenario.id)}</code>: ${html(scenario.title)} ${coverageBadge(scenarioCoverageItem?.covered, [], scenarioCoverageItem, sourceLinks)} ${renderEvidenceSummary(changedCount, unchangedCount)}</summary>
|
|
178
|
+
<div class="scenario-body">${renderScenarioRuleCoverage(scenarioRuleIds)}${scenario.steps.map((step) => renderStep(spec, step, evidenceByLine, sourceLinks)).join("")}</div>
|
|
179
|
+
</details>`;
|
|
180
|
+
}
|
|
181
|
+
function renderEvidenceSummary(changedCount, unchangedCount) {
|
|
182
|
+
if (changedCount === 0 && unchangedCount === 0) {
|
|
183
|
+
return `<span class="badge muted">no visual evidence recorded</span>`;
|
|
204
184
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
185
|
+
const changedLabel = `${changedCount} visual change${changedCount === 1 ? "" : "s"}`;
|
|
186
|
+
const unchangedLabel = `${unchangedCount} unchanged screen${unchangedCount === 1 ? "" : "s"}`;
|
|
187
|
+
return `<span class="badge">${html([changedLabel, unchangedLabel].join(" · "))}</span>`;
|
|
188
|
+
}
|
|
189
|
+
function renderScenarioRuleCoverage(ruleIds) {
|
|
190
|
+
if (!ruleIds.length)
|
|
191
|
+
return `<p><strong>Rules covered by this scenario:</strong> <span class="muted">none referenced</span></p>`;
|
|
192
|
+
return `<p><strong>Rules covered by this scenario:</strong> ${ruleIds.map((ruleId) => `<code>${html(ruleId)}</code>`).join(" ")}</p>`;
|
|
208
193
|
}
|
|
209
194
|
function renderModelItemBody(body) {
|
|
210
195
|
const lines = body.split("\n");
|
|
@@ -242,49 +227,40 @@ function renderModelItemBody(body) {
|
|
|
242
227
|
function renderInlineMarkdown(source) {
|
|
243
228
|
return source
|
|
244
229
|
.split(/(`[^`]+`)/g)
|
|
245
|
-
.map((part) => part.startsWith("`") && part.endsWith("`")
|
|
246
|
-
? `<code>${html(part.slice(1, -1))}</code>`
|
|
247
|
-
: html(part))
|
|
230
|
+
.map((part) => part.startsWith("`") && part.endsWith("`") ? `<code>${html(part.slice(1, -1))}</code>` : html(part))
|
|
248
231
|
.join("");
|
|
249
232
|
}
|
|
250
233
|
function isTableStart(lines, index) {
|
|
251
|
-
return
|
|
252
|
-
index + 1 < lines.length &&
|
|
253
|
-
isTableSeparator(lines[index + 1]));
|
|
234
|
+
return isPipeTableRow(lines[index]) && index + 1 < lines.length && isTableSeparator(lines[index + 1]);
|
|
254
235
|
}
|
|
255
236
|
function isPipeTableRow(line) {
|
|
256
237
|
return line.trim().startsWith("|") && line.trim().endsWith("|");
|
|
257
238
|
}
|
|
258
239
|
function isTableSeparator(line) {
|
|
259
|
-
return
|
|
260
|
-
splitTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell)));
|
|
240
|
+
return isPipeTableRow(line) && splitTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
261
241
|
}
|
|
262
242
|
function splitTableRow(line) {
|
|
263
|
-
return line
|
|
264
|
-
.trim()
|
|
265
|
-
.replace(/^\|/, "")
|
|
266
|
-
.replace(/\|$/, "")
|
|
267
|
-
.split("|")
|
|
268
|
-
.map((cell) => cell.trim());
|
|
243
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
269
244
|
}
|
|
270
245
|
function renderTable(lines) {
|
|
271
246
|
const headers = splitTableRow(lines[0]);
|
|
272
247
|
const rows = lines.slice(1).map(splitTableRow);
|
|
273
|
-
return `<div class="table-wrap"><table><thead><tr>${headers
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.join("")}</tbody></table></div>`;
|
|
280
|
-
}
|
|
281
|
-
function renderStep(spec, step, screenshotsByLine, sourceLinks) {
|
|
282
|
-
const screenshots = screenshotsByLine.get(screenshotKey(spec.filePath, step.line)) ?? [];
|
|
248
|
+
return `<div class="table-wrap"><table><thead><tr>${headers.map((header) => `<th>${renderInlineMarkdown(header)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`;
|
|
249
|
+
}
|
|
250
|
+
function renderStep(spec, step, evidenceByLine, sourceLinks) {
|
|
251
|
+
const evidence = evidenceByLine.get(screenshotKey(spec.filePath, step.line)) ?? [];
|
|
252
|
+
const screenshots = evidence.filter((entry) => entry.changed && entry.path);
|
|
253
|
+
const unchanged = evidence.filter((entry) => !entry.changed);
|
|
283
254
|
const evidenceBadge = screenshots.length
|
|
284
|
-
? `<span class="badge ok"
|
|
285
|
-
:
|
|
255
|
+
? `<span class="badge ok">screen changed · screenshot captured</span>`
|
|
256
|
+
: unchanged.length
|
|
257
|
+
? `<span class="badge muted">same screen${renderComparedWith(unchanged[0])}</span>`
|
|
258
|
+
: `<span class="badge muted" title="missing screenshot evidence is informational">no screenshot captured</span>`;
|
|
286
259
|
return `<div class="step"><p><strong>${html(step.keyword)}</strong> ${html(step.text)} ${renderLineBadge(spec.filePath, step.line, sourceLinks)} ${evidenceBadge}</p>${renderScreenshots(screenshots)}</div>`;
|
|
287
260
|
}
|
|
261
|
+
function renderComparedWith(entry) {
|
|
262
|
+
return entry.comparedWithLine ? ` as line ${html(String(entry.comparedWithLine))}` : " as previous screen";
|
|
263
|
+
}
|
|
288
264
|
function renderLineBadge(filePath, line, sourceLinks) {
|
|
289
265
|
const label = `line ${line}`;
|
|
290
266
|
const url = sourceLineUrl(filePath, line, sourceLinks);
|
|
@@ -295,30 +271,21 @@ function renderLineBadge(filePath, line, sourceLinks) {
|
|
|
295
271
|
function renderScreenshots(screenshots) {
|
|
296
272
|
if (!screenshots.length)
|
|
297
273
|
return "";
|
|
298
|
-
return `<div class="screenshots">${screenshots
|
|
299
|
-
.map((screenshot) => `<figure class="screenshot"><img src="${html(screenshot.path)}" alt="${html(screenshot.title ?? `Screenshot for ${screenshot.specPath}:${screenshot.line}`)}"><figcaption>${html(screenshot.title ?? `${screenshot.specPath}:${screenshot.line}`)}</figcaption></figure>`)
|
|
300
|
-
.join("")}</div>`;
|
|
274
|
+
return `<div class="screenshots">${screenshots.map((screenshot) => `<figure class="screenshot"><img src="${html(screenshot.path ?? "")}" alt="${html(screenshot.title ?? `Screenshot for ${screenshot.specPath}:${screenshot.line}`)}"><figcaption>${html(screenshot.title ?? `${screenshot.specPath}:${screenshot.line}`)}</figcaption></figure>`).join("")}</div>`;
|
|
301
275
|
}
|
|
302
276
|
function coverageBadge(covered, suffixes = [], item, sourceLinks = {}) {
|
|
303
|
-
return covered === undefined
|
|
304
|
-
? ""
|
|
305
|
-
: covered
|
|
306
|
-
? `<span class="badge ok">covered${suffixes.length ? ` by ${suffixes.map(html).join(" ")}` : ""}${renderCoverageReferenceMarkers(item, sourceLinks)}</span>`
|
|
307
|
-
: `<span class="badge missing">missing coverage</span>`;
|
|
277
|
+
return covered === undefined ? "" : covered ? `<span class="badge ok">covered${suffixes.length ? ` by ${suffixes.map(html).join(" ")}` : ""}${renderCoverageReferenceMarkers(item, sourceLinks)}</span>` : `<span class="badge missing">missing coverage</span>`;
|
|
308
278
|
}
|
|
309
279
|
function ruleCoverageBadge(ruleCoverage, scenarioIds, sourceLinks) {
|
|
310
|
-
if (ruleCoverage?.covered && !scenarioIds.length)
|
|
280
|
+
if (ruleCoverage?.covered && !scenarioIds.length)
|
|
311
281
|
return coverageBadge(true, ["direct test"], ruleCoverage, sourceLinks);
|
|
312
|
-
}
|
|
313
282
|
return coverageBadge(ruleCoverage?.covered, scenarioIds, ruleCoverage, sourceLinks);
|
|
314
283
|
}
|
|
315
284
|
function renderCoverageReferenceMarkers(item, sourceLinks) {
|
|
316
285
|
const references = uniqueCoverageReferences(item);
|
|
317
286
|
if (!references.length)
|
|
318
287
|
return "";
|
|
319
|
-
return ` <span class="coverage-refs">${references
|
|
320
|
-
.map((reference, index) => renderCoverageReferenceMarker(reference, index + 1, sourceLinks))
|
|
321
|
-
.join(",")}</span>`;
|
|
288
|
+
return ` <span class="coverage-refs">${references.map((reference, index) => renderCoverageReferenceMarker(reference, index + 1, sourceLinks)).join(",")}</span>`;
|
|
322
289
|
}
|
|
323
290
|
function renderCoverageReferenceMarker(reference, index, sourceLinks) {
|
|
324
291
|
const label = coverageReferenceLabel(reference);
|
|
@@ -353,29 +320,18 @@ function sourceLineUrl(filePath, line, sourceLinks) {
|
|
|
353
320
|
return undefined;
|
|
354
321
|
const baseUrl = sourceLinks.githubBaseUrl.replace(/\/$/, "");
|
|
355
322
|
const ref = encodeURIComponent(sourceLinks.githubRef);
|
|
356
|
-
const encodedFilePath = filePath
|
|
357
|
-
.split("/")
|
|
358
|
-
.map((segment) => encodeURIComponent(segment))
|
|
359
|
-
.join("/");
|
|
323
|
+
const encodedFilePath = filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
360
324
|
return `${baseUrl}/blob/${ref}/${encodedFilePath}#L${line}`;
|
|
361
325
|
}
|
|
362
326
|
function ruleScenarioIds(ruleId, ruleScenarioLinks) {
|
|
363
|
-
return Array.from(new Set(ruleScenarioLinks
|
|
364
|
-
.filter((link) => link.ruleId === ruleId)
|
|
365
|
-
.map((link) => link.scenarioId))).sort();
|
|
327
|
+
return Array.from(new Set(ruleScenarioLinks.filter((link) => link.ruleId === ruleId).map((link) => link.scenarioId))).sort();
|
|
366
328
|
}
|
|
367
329
|
function ruleIdsForScenario(scenarioId, specRuleIds, ruleScenarioLinks) {
|
|
368
|
-
const ruleIds = new Set(ruleScenarioLinks
|
|
369
|
-
.filter((link) => link.scenarioId === scenarioId)
|
|
370
|
-
.map((link) => link.ruleId));
|
|
330
|
+
const ruleIds = new Set(ruleScenarioLinks.filter((link) => link.scenarioId === scenarioId).map((link) => link.ruleId));
|
|
371
331
|
return specRuleIds.filter((ruleId) => ruleIds.has(ruleId));
|
|
372
332
|
}
|
|
373
333
|
function buildRuleScenarioLinks(ruleCoverage, scenarioCoverage) {
|
|
374
|
-
const scenarioReferences = scenarioCoverage.flatMap((scenario) => scenario.references.map((reference) => ({
|
|
375
|
-
scenarioId: scenario.id,
|
|
376
|
-
filePath: reference.filePath,
|
|
377
|
-
line: reference.line,
|
|
378
|
-
})));
|
|
334
|
+
const scenarioReferences = scenarioCoverage.flatMap((scenario) => scenario.references.map((reference) => ({ scenarioId: scenario.id, filePath: reference.filePath, line: reference.line })));
|
|
379
335
|
const links = [];
|
|
380
336
|
const seen = new Set();
|
|
381
337
|
for (const rule of ruleCoverage) {
|
|
@@ -395,16 +351,14 @@ function buildRuleScenarioLinks(ruleCoverage, scenarioCoverage) {
|
|
|
395
351
|
function nearestScenarioReference(filePath, line, scenarioReferences) {
|
|
396
352
|
const maxLineDistance = 8;
|
|
397
353
|
return scenarioReferences
|
|
398
|
-
.filter((reference) => reference.filePath === filePath &&
|
|
399
|
-
reference.line <= line &&
|
|
400
|
-
line - reference.line <= maxLineDistance)
|
|
354
|
+
.filter((reference) => reference.filePath === filePath && reference.line <= line && line - reference.line <= maxLineDistance)
|
|
401
355
|
.sort((left, right) => right.line - left.line)[0];
|
|
402
356
|
}
|
|
403
|
-
function
|
|
357
|
+
function groupEvidenceByLine(evidence) {
|
|
404
358
|
const grouped = new Map();
|
|
405
|
-
for (const
|
|
406
|
-
const key = screenshotKey(
|
|
407
|
-
grouped.set(key, [...(grouped.get(key) ?? []),
|
|
359
|
+
for (const entry of evidence) {
|
|
360
|
+
const key = screenshotKey(entry.specPath, entry.line);
|
|
361
|
+
grouped.set(key, [...(grouped.get(key) ?? []), entry]);
|
|
408
362
|
}
|
|
409
363
|
return grouped;
|
|
410
364
|
}
|