@testivai/witness 1.0.0 → 1.0.2

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 (88) hide show
  1. package/README.md +44 -13
  2. package/dist/baselines/index.d.ts +6 -0
  3. package/dist/baselines/index.d.ts.map +1 -0
  4. package/dist/baselines/index.js +9 -0
  5. package/dist/baselines/index.js.map +1 -0
  6. package/dist/baselines/store.d.ts +79 -0
  7. package/dist/baselines/store.d.ts.map +1 -0
  8. package/dist/baselines/store.js +234 -0
  9. package/dist/baselines/store.js.map +1 -0
  10. package/dist/bin/testivai.js +2 -0
  11. package/dist/bin/testivai.js.map +1 -1
  12. package/dist/browser/binding.d.ts.map +1 -1
  13. package/dist/browser/binding.js +173 -42
  14. package/dist/browser/binding.js.map +1 -1
  15. package/dist/browser/capture.d.ts.map +1 -1
  16. package/dist/browser/capture.js +86 -38
  17. package/dist/browser/capture.js.map +1 -1
  18. package/dist/commands/approve.d.ts +3 -0
  19. package/dist/commands/approve.d.ts.map +1 -0
  20. package/dist/commands/approve.js +94 -0
  21. package/dist/commands/approve.js.map +1 -0
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +53 -0
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/commands/run.d.ts.map +1 -1
  26. package/dist/commands/run.js +98 -0
  27. package/dist/commands/run.js.map +1 -1
  28. package/dist/config/index.d.ts +6 -0
  29. package/dist/config/index.d.ts.map +1 -0
  30. package/dist/config/index.js +14 -0
  31. package/dist/config/index.js.map +1 -0
  32. package/dist/config/local-config.d.ts +54 -0
  33. package/dist/config/local-config.d.ts.map +1 -0
  34. package/dist/config/local-config.js +130 -0
  35. package/dist/config/local-config.js.map +1 -0
  36. package/dist/diff/diff.d.ts +27 -0
  37. package/dist/diff/diff.d.ts.map +1 -0
  38. package/dist/diff/diff.js +216 -0
  39. package/dist/diff/diff.js.map +1 -0
  40. package/dist/diff/ignore.d.ts +22 -0
  41. package/dist/diff/ignore.d.ts.map +1 -0
  42. package/dist/diff/ignore.js +43 -0
  43. package/dist/diff/ignore.js.map +1 -0
  44. package/dist/diff/index.d.ts +16 -0
  45. package/dist/diff/index.d.ts.map +1 -0
  46. package/dist/diff/index.js +23 -0
  47. package/dist/diff/index.js.map +1 -0
  48. package/dist/diff/minimap.d.ts +23 -0
  49. package/dist/diff/minimap.d.ts.map +1 -0
  50. package/dist/diff/minimap.js +43 -0
  51. package/dist/diff/minimap.js.map +1 -0
  52. package/dist/diff/regions.d.ts +23 -0
  53. package/dist/diff/regions.d.ts.map +1 -0
  54. package/dist/diff/regions.js +129 -0
  55. package/dist/diff/regions.js.map +1 -0
  56. package/dist/diff/resize.d.ts +27 -0
  57. package/dist/diff/resize.d.ts.map +1 -0
  58. package/dist/diff/resize.js +82 -0
  59. package/dist/diff/resize.js.map +1 -0
  60. package/dist/diff/types.d.ts +79 -0
  61. package/dist/diff/types.d.ts.map +1 -0
  62. package/dist/diff/types.js +10 -0
  63. package/dist/diff/types.js.map +1 -0
  64. package/dist/index.d.ts +8 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +23 -2
  67. package/dist/index.js.map +1 -1
  68. package/dist/report/compare.d.ts +18 -0
  69. package/dist/report/compare.d.ts.map +1 -0
  70. package/dist/report/compare.js +163 -0
  71. package/dist/report/compare.js.map +1 -0
  72. package/dist/report/generator.d.ts +31 -0
  73. package/dist/report/generator.d.ts.map +1 -0
  74. package/dist/report/generator.js +123 -0
  75. package/dist/report/generator.js.map +1 -0
  76. package/dist/report/index.d.ts +10 -0
  77. package/dist/report/index.d.ts.map +1 -0
  78. package/dist/report/index.js +13 -0
  79. package/dist/report/index.js.map +1 -0
  80. package/dist/report/results.d.ts +35 -0
  81. package/dist/report/results.d.ts.map +1 -0
  82. package/dist/report/results.js +8 -0
  83. package/dist/report/results.js.map +1 -0
  84. package/dist/report/template.d.ts +14 -0
  85. package/dist/report/template.d.ts.map +1 -0
  86. package/dist/report/template.js +229 -0
  87. package/dist/report/template.js.map +1 -0
  88. package/package.json +58 -15
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI Report — Generator
4
+ *
5
+ * Orchestrator: compareAll → build ReportData → write results.json
6
+ * → render HTML → optionally auto-open browser.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.generateReport = generateReport;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const child_process_1 = require("child_process");
46
+ const compare_1 = require("./compare");
47
+ const template_1 = require("./template");
48
+ /**
49
+ * Generate a complete visual regression report.
50
+ *
51
+ * 1. Compare all temp screenshots against baselines
52
+ * 2. Build ReportData with summary
53
+ * 3. Write results.json (machine-readable)
54
+ * 4. Render and write index.html
55
+ * 5. Optionally open in browser
56
+ *
57
+ * @returns The report data
58
+ */
59
+ function generateReport(options) {
60
+ const { projectRoot, reportDir: reportDirRelative = 'visual-report', threshold = 0.1, autoOpen = true, version = '2.0.0', } = options;
61
+ const reportDir = path.isAbsolute(reportDirRelative)
62
+ ? reportDirRelative
63
+ : path.join(projectRoot, reportDirRelative);
64
+ // Ensure report directory exists
65
+ fs.mkdirSync(reportDir, { recursive: true });
66
+ // 1. Compare all snapshots
67
+ const compareOptions = {
68
+ projectRoot,
69
+ reportDir,
70
+ threshold,
71
+ };
72
+ const snapshots = (0, compare_1.compareAll)(compareOptions);
73
+ // 2. Build summary
74
+ const summary = {
75
+ total: snapshots.length,
76
+ passed: snapshots.filter((s) => s.status === 'passed').length,
77
+ changed: snapshots.filter((s) => s.status === 'changed').length,
78
+ newSnapshots: snapshots.filter((s) => s.status === 'new').length,
79
+ };
80
+ // 3. Build report data
81
+ const reportData = {
82
+ version,
83
+ timestamp: new Date().toISOString(),
84
+ summary,
85
+ snapshots,
86
+ };
87
+ // 4. Write results.json
88
+ const resultsPath = path.join(reportDir, 'results.json');
89
+ fs.writeFileSync(resultsPath, JSON.stringify(reportData, null, 2));
90
+ // 5. Render and write HTML
91
+ const html = (0, template_1.renderHtml)(reportData);
92
+ const htmlPath = path.join(reportDir, 'index.html');
93
+ fs.writeFileSync(htmlPath, html);
94
+ // 6. Auto-open browser
95
+ if (autoOpen) {
96
+ openInBrowser(htmlPath);
97
+ }
98
+ return reportData;
99
+ }
100
+ /**
101
+ * Open a file in the default browser.
102
+ * Best-effort — does not throw on failure.
103
+ */
104
+ function openInBrowser(filePath) {
105
+ const url = `file://${filePath}`;
106
+ const platform = process.platform;
107
+ let cmd;
108
+ if (platform === 'darwin') {
109
+ cmd = `open "${url}"`;
110
+ }
111
+ else if (platform === 'win32') {
112
+ cmd = `start "" "${url}"`;
113
+ }
114
+ else {
115
+ cmd = `xdg-open "${url}"`;
116
+ }
117
+ (0, child_process_1.exec)(cmd, (err) => {
118
+ if (err) {
119
+ // Silently ignore — CI environments won't have a browser
120
+ }
121
+ });
122
+ }
123
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/report/generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * TestivAI Report Generator
3
+ */
4
+ export { generateReport } from './generator';
5
+ export type { GenerateReportOptions } from './generator';
6
+ export { compareAll } from './compare';
7
+ export type { CompareOptions } from './compare';
8
+ export { renderHtml } from './template';
9
+ export type { ReportData, ReportSummary, SnapshotResult, SnapshotStatus, } from './results';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,WAAW,CAAC"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI Report Generator
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderHtml = exports.compareAll = exports.generateReport = void 0;
7
+ var generator_1 = require("./generator");
8
+ Object.defineProperty(exports, "generateReport", { enumerable: true, get: function () { return generator_1.generateReport; } });
9
+ var compare_1 = require("./compare");
10
+ Object.defineProperty(exports, "compareAll", { enumerable: true, get: function () { return compare_1.compareAll; } });
11
+ var template_1 = require("./template");
12
+ Object.defineProperty(exports, "renderHtml", { enumerable: true, get: function () { return template_1.renderHtml; } });
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,yCAA6C;AAApC,2GAAA,cAAc,OAAA;AAEvB,qCAAuC;AAA9B,qGAAA,UAAU,OAAA;AAEnB,uCAAwC;AAA/B,sGAAA,UAAU,OAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * TestivAI Report — Result types
3
+ *
4
+ * Machine-readable output for the GitHub Action and other consumers.
5
+ */
6
+ export type SnapshotStatus = 'passed' | 'changed' | 'new';
7
+ export interface SnapshotResult {
8
+ name: string;
9
+ status: SnapshotStatus;
10
+ /** Diff percentage (0-100). 0 for passed/new. */
11
+ diffPercent: number;
12
+ /** Number of changed pixels */
13
+ diffCount: number;
14
+ /** Total pixels compared */
15
+ totalPixels: number;
16
+ /** Relative path to baseline image (if any) */
17
+ baselinePath?: string;
18
+ /** Relative path to current (candidate) image */
19
+ currentPath?: string;
20
+ /** Relative path to diff image (if any) */
21
+ diffPath?: string;
22
+ }
23
+ export interface ReportSummary {
24
+ total: number;
25
+ passed: number;
26
+ changed: number;
27
+ newSnapshots: number;
28
+ }
29
+ export interface ReportData {
30
+ version: string;
31
+ timestamp: string;
32
+ summary: ReportSummary;
33
+ snapshots: SnapshotResult[];
34
+ }
35
+ //# sourceMappingURL=results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/report/results.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI Report — Result types
4
+ *
5
+ * Machine-readable output for the GitHub Action and other consumers.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ //# sourceMappingURL=results.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.js","sourceRoot":"","sources":["../../src/report/results.ts"],"names":[],"mappings":";AAAA;;;;GAIG"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * TestivAI Report — HTML Template
3
+ *
4
+ * Single-file HTML with inlined CSS/JS.
5
+ * Dark navy sidebar, cyan accents, summary cards,
6
+ * Changed → New → Passed sections, 3-column diff view,
7
+ * approve command copy button, cloud upgrade CTA, image zoom overlay.
8
+ */
9
+ import { ReportData } from './results';
10
+ /**
11
+ * Render a standalone HTML report from report data.
12
+ */
13
+ export declare function renderHtml(data: ReportData): string;
14
+ //# sourceMappingURL=template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/report/template.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAkB,MAAM,WAAW,CAAC;AAEvD;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAgLnD"}
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI Report — HTML Template
4
+ *
5
+ * Single-file HTML with inlined CSS/JS.
6
+ * Dark navy sidebar, cyan accents, summary cards,
7
+ * Changed → New → Passed sections, 3-column diff view,
8
+ * approve command copy button, cloud upgrade CTA, image zoom overlay.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.renderHtml = renderHtml;
12
+ /**
13
+ * Render a standalone HTML report from report data.
14
+ */
15
+ function renderHtml(data) {
16
+ const { summary, snapshots, version, timestamp } = data;
17
+ const changed = snapshots.filter((s) => s.status === 'changed');
18
+ const newSnapshots = snapshots.filter((s) => s.status === 'new');
19
+ const passed = snapshots.filter((s) => s.status === 'passed');
20
+ return `<!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="UTF-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
25
+ <title>TestivAI Visual Report</title>
26
+ <style>
27
+ * { margin: 0; padding: 0; box-sizing: border-box; }
28
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #e6edf3; display: flex; min-height: 100vh; }
29
+
30
+ /* Sidebar */
31
+ .sidebar { width: 260px; background: #08111f; padding: 24px 16px; flex-shrink: 0; border-right: 1px solid #21262d; position: fixed; height: 100vh; overflow-y: auto; }
32
+ .sidebar h1 { font-size: 18px; color: #00d4ff; margin-bottom: 4px; }
33
+ .sidebar .version { font-size: 12px; color: #8b949e; margin-bottom: 24px; }
34
+ .sidebar .timestamp { font-size: 11px; color: #8b949e; margin-bottom: 24px; }
35
+
36
+ /* Summary cards */
37
+ .summary-cards { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
38
+ .card { padding: 12px; border-radius: 8px; background: #161b22; border: 1px solid #21262d; }
39
+ .card .label { font-size: 11px; text-transform: uppercase; color: #8b949e; letter-spacing: 0.5px; }
40
+ .card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }
41
+ .card.total .value { color: #e6edf3; }
42
+ .card.changed .value { color: #f85149; }
43
+ .card.new .value { color: #f0883e; }
44
+ .card.passed .value { color: #3fb950; }
45
+
46
+ /* Nav */
47
+ .nav { list-style: none; margin-bottom: 24px; }
48
+ .nav li { margin-bottom: 2px; }
49
+ .nav a { display: block; padding: 8px 12px; border-radius: 6px; color: #8b949e; text-decoration: none; font-size: 13px; transition: all 0.15s; }
50
+ .nav a:hover { background: #161b22; color: #e6edf3; }
51
+ .nav a .count { float: right; background: #21262d; padding: 2px 8px; border-radius: 10px; font-size: 11px; }
52
+
53
+ /* CTA */
54
+ .cta { background: linear-gradient(135deg, #0d419d 0%, #1158d9 100%); padding: 16px; border-radius: 8px; margin-top: auto; }
55
+ .cta h3 { font-size: 13px; color: #fff; margin-bottom: 6px; }
56
+ .cta p { font-size: 11px; color: #a5d6ff; line-height: 1.5; }
57
+ .cta a { display: inline-block; margin-top: 8px; padding: 6px 14px; background: #00d4ff; color: #08111f; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: 600; }
58
+
59
+ /* Main content */
60
+ .main { margin-left: 260px; flex: 1; padding: 32px; }
61
+ .section { margin-bottom: 48px; }
62
+ .section-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #21262d; }
63
+ .section-title.changed { color: #f85149; }
64
+ .section-title.new { color: #f0883e; }
65
+ .section-title.passed { color: #3fb950; }
66
+
67
+ /* Snapshot card */
68
+ .snapshot { background: #161b22; border: 1px solid #21262d; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
69
+ .snapshot-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
70
+ .snapshot-name { font-size: 16px; font-weight: 600; }
71
+ .snapshot-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
72
+ .badge-changed { background: #f8514922; color: #f85149; }
73
+ .badge-new { background: #f0883e22; color: #f0883e; }
74
+ .badge-passed { background: #3fb95022; color: #3fb950; }
75
+ .snapshot-stats { font-size: 12px; color: #8b949e; margin-bottom: 16px; }
76
+
77
+ /* 3-column diff view */
78
+ .diff-view { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
79
+ .diff-view.two-col { grid-template-columns: 1fr 1fr; }
80
+ .diff-view.one-col { grid-template-columns: 1fr; }
81
+ .diff-col { text-align: center; }
82
+ .diff-col label { display: block; font-size: 11px; color: #8b949e; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
83
+ .diff-col img { max-width: 100%; border-radius: 6px; border: 1px solid #21262d; cursor: zoom-in; transition: transform 0.2s; }
84
+ .diff-col img:hover { transform: scale(1.02); }
85
+
86
+ /* Approve command */
87
+ .approve-cmd { margin-top: 16px; background: #0d1117; border: 1px solid #21262d; border-radius: 8px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; }
88
+ .approve-cmd code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #00d4ff; }
89
+ .approve-cmd button { background: #21262d; border: 1px solid #30363d; color: #e6edf3; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.15s; }
90
+ .approve-cmd button:hover { background: #30363d; }
91
+ .approve-cmd button.copied { background: #3fb950; color: #0d1117; }
92
+
93
+ /* Zoom overlay */
94
+ .zoom-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 100; justify-content: center; align-items: center; cursor: zoom-out; }
95
+ .zoom-overlay.active { display: flex; }
96
+ .zoom-overlay img { max-width: 95%; max-height: 95%; border-radius: 8px; }
97
+
98
+ /* Empty state */
99
+ .empty { text-align: center; padding: 48px; color: #8b949e; }
100
+ .empty .icon { font-size: 48px; margin-bottom: 16px; }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <aside class="sidebar">
105
+ <h1>TestivAI</h1>
106
+ <div class="version">Visual Report v${escapeHtml(version)}</div>
107
+ <div class="timestamp">${escapeHtml(new Date(timestamp).toLocaleString())}</div>
108
+
109
+ <div class="summary-cards">
110
+ <div class="card total"><div class="label">Total</div><div class="value">${summary.total}</div></div>
111
+ <div class="card changed"><div class="label">Changed</div><div class="value">${summary.changed}</div></div>
112
+ <div class="card new"><div class="label">New</div><div class="value">${summary.newSnapshots}</div></div>
113
+ <div class="card passed"><div class="label">Passed</div><div class="value">${summary.passed}</div></div>
114
+ </div>
115
+
116
+ <ul class="nav">
117
+ ${summary.changed > 0 ? `<li><a href="#changed">Changed <span class="count">${summary.changed}</span></a></li>` : ''}
118
+ ${summary.newSnapshots > 0 ? `<li><a href="#new">New <span class="count">${summary.newSnapshots}</span></a></li>` : ''}
119
+ ${summary.passed > 0 ? `<li><a href="#passed">Passed <span class="count">${summary.passed}</span></a></li>` : ''}
120
+ </ul>
121
+
122
+ <div class="cta">
123
+ <h3>Need AI-powered analysis?</h3>
124
+ <p>TestivAI Cloud adds smart matching, AI verdicts, and team collaboration.</p>
125
+ <a href="https://testiv.ai" target="_blank">Learn More</a>
126
+ </div>
127
+ </aside>
128
+
129
+ <main class="main">
130
+ ${summary.total === 0 ? `
131
+ <div class="empty">
132
+ <div class="icon">📸</div>
133
+ <h2>No snapshots found</h2>
134
+ <p>Run your tests to generate visual snapshots.</p>
135
+ </div>` : ''}
136
+
137
+ ${changed.length > 0 ? `
138
+ <section class="section" id="changed">
139
+ <h2 class="section-title changed">Changed (${changed.length})</h2>
140
+ ${changed.map((s) => renderSnapshot(s)).join('\n')}
141
+ </section>` : ''}
142
+
143
+ ${newSnapshots.length > 0 ? `
144
+ <section class="section" id="new">
145
+ <h2 class="section-title new">New (${newSnapshots.length})</h2>
146
+ ${newSnapshots.map((s) => renderSnapshot(s)).join('\n')}
147
+ </section>` : ''}
148
+
149
+ ${passed.length > 0 ? `
150
+ <section class="section" id="passed">
151
+ <h2 class="section-title passed">Passed (${passed.length})</h2>
152
+ ${passed.map((s) => renderSnapshot(s)).join('\n')}
153
+ </section>` : ''}
154
+ </main>
155
+
156
+ <div class="zoom-overlay" id="zoomOverlay">
157
+ <img id="zoomImage" src="" alt="Zoomed">
158
+ </div>
159
+
160
+ <script>
161
+ // Image zoom
162
+ document.querySelectorAll('.diff-col img').forEach(img => {
163
+ img.addEventListener('click', () => {
164
+ document.getElementById('zoomImage').src = img.src;
165
+ document.getElementById('zoomOverlay').classList.add('active');
166
+ });
167
+ });
168
+ document.getElementById('zoomOverlay').addEventListener('click', () => {
169
+ document.getElementById('zoomOverlay').classList.remove('active');
170
+ });
171
+
172
+ // Copy approve command
173
+ document.querySelectorAll('.copy-btn').forEach(btn => {
174
+ btn.addEventListener('click', () => {
175
+ const cmd = btn.dataset.cmd;
176
+ navigator.clipboard.writeText(cmd).then(() => {
177
+ btn.textContent = 'Copied!';
178
+ btn.classList.add('copied');
179
+ setTimeout(() => {
180
+ btn.textContent = 'Copy';
181
+ btn.classList.remove('copied');
182
+ }, 2000);
183
+ });
184
+ });
185
+ });
186
+ </script>
187
+ </body>
188
+ </html>`;
189
+ }
190
+ function renderSnapshot(snapshot) {
191
+ const badgeClass = snapshot.status === 'changed' ? 'badge-changed' : snapshot.status === 'new' ? 'badge-new' : 'badge-passed';
192
+ const hasBaseline = !!snapshot.baselinePath;
193
+ const hasDiff = !!snapshot.diffPath;
194
+ const hasCurrent = !!snapshot.currentPath;
195
+ let gridClass = 'diff-view';
196
+ if (hasBaseline && hasDiff && hasCurrent)
197
+ gridClass += '';
198
+ else if (hasCurrent && !hasBaseline)
199
+ gridClass += ' one-col';
200
+ else
201
+ gridClass += ' two-col';
202
+ return `
203
+ <div class="snapshot">
204
+ <div class="snapshot-header">
205
+ <span class="snapshot-name">${escapeHtml(snapshot.name)}</span>
206
+ <span class="snapshot-badge ${badgeClass}">${snapshot.status}</span>
207
+ </div>
208
+ ${snapshot.status === 'changed' ? `<div class="snapshot-stats">Diff: ${snapshot.diffPercent.toFixed(2)}% (${snapshot.diffCount} pixels)</div>` : ''}
209
+ <div class="${gridClass}">
210
+ ${hasBaseline ? `<div class="diff-col"><label>Baseline</label><img src="${snapshot.baselinePath}" alt="Baseline"></div>` : ''}
211
+ ${hasDiff ? `<div class="diff-col"><label>Diff</label><img src="${snapshot.diffPath}" alt="Diff"></div>` : ''}
212
+ ${hasCurrent ? `<div class="diff-col"><label>Current</label><img src="${snapshot.currentPath}" alt="Current"></div>` : ''}
213
+ </div>
214
+ ${snapshot.status === 'changed' || snapshot.status === 'new' ? `
215
+ <div class="approve-cmd">
216
+ <code>npx testivai approve ${escapeHtml(snapshot.name)}</code>
217
+ <button class="copy-btn" data-cmd="npx testivai approve ${escapeHtml(snapshot.name)}">Copy</button>
218
+ </div>` : ''}
219
+ </div>`;
220
+ }
221
+ function escapeHtml(str) {
222
+ return str
223
+ .replace(/&/g, '&amp;')
224
+ .replace(/</g, '&lt;')
225
+ .replace(/>/g, '&gt;')
226
+ .replace(/"/g, '&quot;')
227
+ .replace(/'/g, '&#039;');
228
+ }
229
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/report/template.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,46 @@
1
1
  {
2
2
  "name": "@testivai/witness",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Framework-agnostic visual regression testing CLI for TestivAI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
- ".": "./dist/index.js"
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./diff": {
13
+ "types": "./dist/diff/index.d.ts",
14
+ "default": "./dist/diff/index.js"
15
+ },
16
+ "./baselines": {
17
+ "types": "./dist/baselines/index.d.ts",
18
+ "default": "./dist/baselines/index.js"
19
+ },
20
+ "./config": {
21
+ "types": "./dist/config/index.d.ts",
22
+ "default": "./dist/config/index.js"
23
+ },
24
+ "./report": {
25
+ "types": "./dist/report/index.d.ts",
26
+ "default": "./dist/report/index.js"
27
+ }
28
+ },
29
+ "typesVersions": {
30
+ "*": {
31
+ "diff": [
32
+ "dist/diff/index.d.ts"
33
+ ],
34
+ "baselines": [
35
+ "dist/baselines/index.d.ts"
36
+ ],
37
+ "config": [
38
+ "dist/config/index.d.ts"
39
+ ],
40
+ "report": [
41
+ "dist/report/index.d.ts"
42
+ ]
43
+ }
9
44
  },
10
45
  "bin": {
11
46
  "testivai": "./dist/bin/testivai.js"
@@ -15,16 +50,6 @@
15
50
  "README.md",
16
51
  "LICENSE"
17
52
  ],
18
- "scripts": {
19
- "build": "tsc",
20
- "clean": "rm -rf dist",
21
- "dev": "tsc --watch",
22
- "prepublishOnly": "npm run build",
23
- "test": "jest",
24
- "test:unit": "jest",
25
- "test:watch": "jest --watch",
26
- "test:coverage": "jest --coverage"
27
- },
28
53
  "keywords": [
29
54
  "testivai",
30
55
  "visual-regression",
@@ -38,18 +63,27 @@
38
63
  ],
39
64
  "author": "TestivAI",
40
65
  "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/mcbuddy/testivai-oss.git",
69
+ "directory": "packages/witness"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/mcbuddy/testivai-oss/issues"
73
+ },
74
+ "homepage": "https://github.com/mcbuddy/testivai-oss/tree/main/packages/witness#readme",
41
75
  "publishConfig": {
42
76
  "access": "public"
43
77
  },
44
78
  "dependencies": {
45
- "@testivai/common": "^0.2.0",
46
79
  "chalk": "^4.1.2",
47
80
  "chrome-remote-interface": "^0.33.0",
48
81
  "commander": "^11.0.0",
49
82
  "inquirer": "^8.2.6",
50
83
  "ora": "^7.0.0",
51
84
  "simple-git": "^3.20.0",
52
- "ws": "^8.0.0"
85
+ "ws": "^8.0.0",
86
+ "@testivai/common": "0.2.2"
53
87
  },
54
88
  "devDependencies": {
55
89
  "@types/inquirer": "^8.2.10",
@@ -62,5 +96,14 @@
62
96
  },
63
97
  "engines": {
64
98
  "node": ">=18.0.0"
99
+ },
100
+ "scripts": {
101
+ "build": "tsc",
102
+ "clean": "rm -rf dist",
103
+ "dev": "tsc --watch",
104
+ "test": "jest",
105
+ "test:unit": "jest",
106
+ "test:watch": "jest --watch",
107
+ "test:coverage": "jest --coverage"
65
108
  }
66
- }
109
+ }