@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.
Files changed (48) hide show
  1. package/dist/cli.js +35 -141
  2. package/dist/cli.js.map +1 -1
  3. package/dist/githubActionDiffReport.d.ts.map +1 -1
  4. package/dist/githubActionDiffReport.js +81 -15
  5. package/dist/githubActionDiffReport.js.map +1 -1
  6. package/dist/githubActionReport.d.ts.map +1 -1
  7. package/dist/githubActionReport.js +27 -22
  8. package/dist/githubActionReport.js.map +1 -1
  9. package/dist/githubReportMetadata.d.ts +7 -0
  10. package/dist/githubReportMetadata.d.ts.map +1 -0
  11. package/dist/githubReportMetadata.js +152 -0
  12. package/dist/githubReportMetadata.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/mockReports.d.ts.map +1 -1
  18. package/dist/mockReports.js +30 -0
  19. package/dist/mockReports.js.map +1 -1
  20. package/dist/playwright.d.ts +3 -3
  21. package/dist/playwright.d.ts.map +1 -1
  22. package/dist/playwright.js +62 -15
  23. package/dist/playwright.js.map +1 -1
  24. package/dist/reportArtifacts.d.ts +7 -0
  25. package/dist/reportArtifacts.d.ts.map +1 -0
  26. package/dist/reportArtifacts.js +26 -0
  27. package/dist/reportArtifacts.js.map +1 -0
  28. package/dist/reportHtml.d.ts +11 -0
  29. package/dist/reportHtml.d.ts.map +1 -0
  30. package/dist/reportHtml.js +33 -0
  31. package/dist/reportHtml.js.map +1 -0
  32. package/dist/reportTemplate.d.ts +0 -1
  33. package/dist/reportTemplate.d.ts.map +1 -1
  34. package/dist/reportTemplate.js +146 -192
  35. package/dist/reportTemplate.js.map +1 -1
  36. package/dist/reportTemplates/shared.d.ts +6 -0
  37. package/dist/reportTemplates/shared.d.ts.map +1 -0
  38. package/dist/reportTemplates/shared.js +15 -0
  39. package/dist/reportTemplates/shared.js.map +1 -0
  40. package/dist/screenshots.d.ts +1 -3
  41. package/dist/screenshots.d.ts.map +1 -1
  42. package/dist/screenshots.js +20 -23
  43. package/dist/screenshots.js.map +1 -1
  44. package/dist/types.d.ts +3 -23
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/src/mocks/current/screenshots/screenshots.json +19 -1
  48. package/src/mocks/previous/screenshots/screenshots.json +10 -1
@@ -1 +1 @@
1
- {"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAkDvD,MAAM,UAAU,4BAA4B,CAC1C,IAAwB,EACxB,OAAsC;IAEtC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAC5B,GAAG,EACH,OAAO,CAAC,SAAS,IAAI,0BAA0B,CAChD,CAAC;IACF,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,aAAa,CAAC;IACvE,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;IAC/D,MAAM,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,eAAe,GAAG,IAAI,GAAG,EAAoC,CAAC;IACpE,MAAM,qBAAqB,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE7D,OAAO;QACL,KAAK,CAAC,QAAQ,CACZ,IAAwB,EACxB,QAAgC,EAChC,UAAkB,EAClB,QAAgB,EAChB,IAAyB;YAEzB,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,YAAY,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;YACpE,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,IAAI,EAAE;gBACzD,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;gBACtD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACzC,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;oBACtE,qBAAqB,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;gBAC1E,CAAC;gBAED,MAAM,IAAI,EAAE,CAAC;gBAEb,MAAM,mBAAmB,CAAC;oBACxB,eAAe;oBACf,IAAI;oBACJ,SAAS;oBACT,aAAa;oBACb,kBAAkB;oBAClB,qBAAqB;oBACrB,QAAQ;oBACR,IAAI;oBACJ,QAAQ;iBACT,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAkB,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACzE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAClC,MAAM,KAAK,GAAuB,EAAE,CAAC;IAErC,IAAI,CAAC;QACH,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,KAAK,MAAM,QAAQ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;YACnE,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YAE9D,KAAK,CAAC,IAAI,CACR,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CACrC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAC5B,UAAU,EAAE,QAAQ,CAAC,EAAE;gBACvB,QAAQ;gBACR,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC,CAAC,CACJ,CACF,CAAC;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,YAAyC,EACzC,UAAkB,EAClB,QAAgB;IAEhB,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC;IACjC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,OAAO,CACzC,+BAA+B,EAC/B,EAAE,CACH,CAAC;IACF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CACrB,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,CAAC,UAAU,KAAK,UAAU;QACnC,SAAS,CAAC,IAAI,KAAK,kBAAkB,CACxC,CAAC;IACF,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,0BAA0B,UAAU,KAAK,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,OAUlC;IACC,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAC3C,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,SAAS,EACjB,QAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAC5B,CAAC;IACF,MAAM,WAAW,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1E,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAE7G,IAAI,aAAa,EAAE,IAAI,KAAK,WAAW,IAAI,aAAa,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5E,MAAM,cAAc,CAAC,OAAO,EAAE;YAC5B,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ;YAC/B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI;YACvB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,KAAK,cAAc;YAC7B,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;YAC/B,gBAAgB,EAAE,aAAa,CAAC,IAAI;SACrC,CAAC,CAAC;QACH,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE;YAClD,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI;SACxB,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,SAAS,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IACvG,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;IAClE,MAAM,YAAY,GAAG,GAAG,OAAO,CAAC,kBAAkB,IAAI,QAAQ,EAAE,CAAC;IACjE,MAAM,SAAS,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IAC/C,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE;QACnC,WAAW,EAAE,WAAW;QACxB,IAAI,EAAE,cAAc;KACrB,CAAC,CAAC;IACH,MAAM,cAAc,CAAC,OAAO,EAAE;QAC5B,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ;QAC/B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI;QACvB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,YAAY;QAClB,KAAK;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;KAChC,CAAC,CAAC;IACH,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE;QAClD,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI;KACxB,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,OAIC,EACD,KAA6B;IAE7B,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAChF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnE,MAAM,mBAAmB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACtF,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,IAAwB,EACxB,SAAiB,EACjB,KAAa;IAEb,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;IAC7D,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvF,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1D,OAAO,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,MAAc;IACpC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,cAAc,CAAC,QAAgC,EAAE,UAAkB;IAC1E,OAAO,GAAG,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,IAAI,IAAI,UAAU,EAAE,CAAC;AAClE,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,SAAiB,EACjB,WAAmB,EACnB,QAAkC;IAElC,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,SAAS,CACb,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,WAAW,OAAO,CAAC,EACvD,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EACrC,MAAM,CACP,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC"}
@@ -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"}
@@ -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":"AAMA,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,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,GAAE,aAAkB,UAuE5B"}
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"}
@@ -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 issues = options.validationIssues ?? [];
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 `<!doctype html>
17
- <html lang="en">
18
- <head>
19
- <meta charset="utf-8">
20
- <meta name="viewport" content="width=device-width,initial-scale=1">
21
- <title>${html(title)}</title>
22
- <style>
23
- body{font-family:system-ui,sans-serif;max-width:1180px;margin:0 auto;padding:40px 24px;color:#1f2328}
24
- .panel{border:1px solid #d0d7de;border-radius:8px;padding:20px;margin:18px 0}
25
- .ok{color:#1a7f37}.missing,.error{color:#cf222e}.warning{color:#9a6700}
26
- .badge{border:1px solid #d0d7de;border-radius:999px;padding:2px 8px;font-size:12px;white-space:nowrap}
27
- .feature-header{display:flex;gap:12px;align-items:center;justify-content:space-between}
28
- .scenario{border:1px solid #d0d7de;border-radius:8px;margin:12px 0;background:#fff}
29
- .scenario summary{cursor:pointer;padding:14px 16px;font-weight:600}
30
- .scenario-body{padding:0 16px 16px}
31
- .model-item{border:1px solid #d0d7de;border-radius:8px;margin:12px 0;background:#fff}
32
- .model-item summary{cursor:pointer;padding:14px 16px;font-weight:600}
33
- .model-item-body{padding:0 16px 16px}
34
- .model-item-body p{margin:8px 0}
35
- .table-wrap{overflow-x:auto;margin:12px 0}
36
- table{border-collapse:collapse;width:100%;font-size:14px}
37
- th,td{border:1px solid #d0d7de;padding:6px 8px;text-align:left;vertical-align:top}
38
- th{background:#f6f8fa}
39
- h1 a{color:#0969da;text-decoration:underline;text-underline-offset:3px}
40
- h1 a:hover{text-decoration-thickness:2px}
41
- .step{border-left:3px solid #d0d7de;margin:12px 0;padding:2px 0 2px 12px}
42
- .step p{margin:8px 0}
43
- .screenshots{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin:10px 0 14px}
44
- .screenshot{border:1px solid #d0d7de;border-radius:8px;overflow:hidden;background:#f6f8fa}
45
- .screenshot img{display:block;width:100%;height:auto}
46
- .screenshot figcaption{font-size:12px;padding:8px;color:#57606a}
47
- .coverage-refs{display:inline-flex;gap:2px;margin-left:4px}
48
- .coverage-ref{color:inherit;text-decoration:underline;text-underline-offset:2px}
49
- .line-link{color:inherit;text-decoration:underline;text-underline-offset:2px}
50
- </style>
51
- </head>
52
- <body>
53
- <h1>${renderReportTitle(title, options.repositoryUrl)}</h1>
54
- <p>Generated ${html(formatGeneratedAt(options.generatedAt))}.</p>
55
- ${renderIssues(issues)}
56
- ${renderModels(options.models ?? [], options.coverage, sourceLinks)}
57
- ${specs.map((spec) => renderSpec(spec, options.coverage, screenshots, sourceLinks)).join("\n")}
58
- <script>
59
- document.addEventListener("toggle", (event) => {
60
- const target = event.target;
61
- if (!(target instanceof HTMLDetailsElement)) return;
62
- if (!target.open || target.dataset.hasImages !== "true") return;
63
- const topBefore = target.getBoundingClientRect().top;
64
- document
65
- .querySelectorAll('details.scenario[data-has-images="true"][open]')
66
- .forEach((details) => {
67
- if (details !== target) details.removeAttribute("open");
68
- });
69
- requestAnimationFrame(() => {
70
- const topAfter = target.getBoundingClientRect().top;
71
- window.scrollBy(0, topAfter - topBefore);
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
- .map((model) => `<section>
140
- <div class="feature-header">
141
- <h3>${html(model.frontmatter.id)} ${html(model.title)}</h3>
142
- <span class="badge">${html(model.frontmatter.status ?? "draft")}</span>
143
- </div>
144
- <p>${html(model.purpose)}</p>
145
- <h4>Model</h4>
146
- ${model.modelItems
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
- <summary><code>${html(item.id)}</code>: ${html(item.title)} ${coverageBadge(coverageItem?.covered, [], coverageItem, sourceLinks)}</summary>
151
- <div class="model-item-body">${renderModelItemBody(item.body)}</div>
152
- </details>`;
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
- ${model.rules.length
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 renderSpec(spec, coverage, screenshots = [], sourceLinks = {}) {
168
- const screenshotsByLine = groupScreenshotsByLine(screenshots);
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 renderScenarioRuleCoverage(ruleIds) {
202
- if (!ruleIds.length) {
203
- return `<p><strong>Rules covered by this scenario:</strong> <span class="missing">none referenced</span></p>`;
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
- return `<p><strong>Rules covered by this scenario:</strong> ${ruleIds
206
- .map((ruleId) => `<code>${html(ruleId)}</code>`)
207
- .join(" ")}</p>`;
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 (isPipeTableRow(lines[index]) &&
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 (isPipeTableRow(line) &&
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
- .map((header) => `<th>${renderInlineMarkdown(header)}</th>`)
275
- .join("")}</tr></thead><tbody>${rows
276
- .map((row) => `<tr>${row
277
- .map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`)
278
- .join("")}</tr>`)
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">${screenshots.length} screenshot${screenshots.length === 1 ? "" : "s"}</span>`
285
- : `<span class="badge missing">missing screenshot</span>`;
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 groupScreenshotsByLine(screenshots) {
357
+ function groupEvidenceByLine(evidence) {
404
358
  const grouped = new Map();
405
- for (const screenshot of screenshots) {
406
- const key = screenshotKey(screenshot.specPath, screenshot.line);
407
- grouped.set(key, [...(grouped.get(key) ?? []), screenshot]);
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
  }