@vizzly-testing/cli 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Static Report Generator
3
+ *
4
+ * Generates a self-contained HTML report from TDD test results using SSR.
5
+ * The report is pre-rendered HTML with inlined CSS - no JavaScript required.
6
+ */
7
+
8
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ let __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ /**
14
+ * Get the path to the dist/reporter directory (for CSS)
15
+ */
16
+ function getReporterDistPath() {
17
+ // Try production path first (when running from dist/)
18
+ let distPath = join(__dirname, '..', 'reporter');
19
+ if (existsSync(join(distPath, 'reporter-bundle.css'))) {
20
+ return distPath;
21
+ }
22
+
23
+ // Fall back to development path (when running from src/)
24
+ distPath = join(__dirname, '..', '..', 'dist', 'reporter');
25
+ if (existsSync(join(distPath, 'reporter-bundle.css'))) {
26
+ return distPath;
27
+ }
28
+ throw new Error('Reporter bundle not found. Run "npm run build:reporter" first.');
29
+ }
30
+
31
+ /**
32
+ * Get the path to the SSR module
33
+ */
34
+ function getSSRModulePath() {
35
+ // Try production path first (when running from dist/)
36
+ let ssrPath = join(__dirname, '..', 'reporter-ssr', 'ssr-entry.js');
37
+ if (existsSync(ssrPath)) {
38
+ return ssrPath;
39
+ }
40
+
41
+ // Fall back to development path (when running from src/)
42
+ ssrPath = join(__dirname, '..', '..', 'dist', 'reporter-ssr', 'ssr-entry.js');
43
+ if (existsSync(ssrPath)) {
44
+ return ssrPath;
45
+ }
46
+ throw new Error('SSR module not found. Run "npm run build:reporter-ssr" first.');
47
+ }
48
+
49
+ /**
50
+ * Recursively copy a directory
51
+ */
52
+ function copyDirectory(src, dest) {
53
+ if (!existsSync(src)) return;
54
+ mkdirSync(dest, {
55
+ recursive: true
56
+ });
57
+ let entries = readdirSync(src);
58
+ for (let entry of entries) {
59
+ let srcPath = join(src, entry);
60
+ let destPath = join(dest, entry);
61
+ let stat = statSync(srcPath);
62
+ if (stat.isDirectory()) {
63
+ copyDirectory(srcPath, destPath);
64
+ } else {
65
+ copyFileSync(srcPath, destPath);
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Transform image URLs from server paths to relative file paths
72
+ * /images/baselines/foo.png -> ./images/baselines/foo.png
73
+ */
74
+ function transformImageUrls(reportData) {
75
+ let transformed = JSON.parse(JSON.stringify(reportData));
76
+ function transformUrl(url) {
77
+ if (!url || typeof url !== 'string') return url;
78
+ if (url.startsWith('/images/')) {
79
+ return `.${url}`;
80
+ }
81
+ return url;
82
+ }
83
+ function transformComparison(comparison) {
84
+ return {
85
+ ...comparison,
86
+ baseline: transformUrl(comparison.baseline),
87
+ current: transformUrl(comparison.current),
88
+ diff: transformUrl(comparison.diff)
89
+ };
90
+ }
91
+ if (transformed.comparisons) {
92
+ transformed.comparisons = transformed.comparisons.map(transformComparison);
93
+ }
94
+ if (transformed.groups) {
95
+ transformed.groups = transformed.groups.map(group => ({
96
+ ...group,
97
+ comparisons: group.comparisons?.map(transformComparison) || []
98
+ }));
99
+ }
100
+ return transformed;
101
+ }
102
+
103
+ /**
104
+ * Generate the static HTML document with SSR-rendered content
105
+ */
106
+ function generateHtml(renderedContent, css) {
107
+ return `<!DOCTYPE html>
108
+ <html lang="en">
109
+ <head>
110
+ <meta charset="UTF-8">
111
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
112
+ <title>Vizzly Visual Test Report</title>
113
+ <style>
114
+ ${css}
115
+ </style>
116
+ </head>
117
+ <body>
118
+ ${renderedContent}
119
+ </body>
120
+ </html>`;
121
+ }
122
+
123
+ /**
124
+ * Generate a static report from the current TDD results
125
+ *
126
+ * @param {string} workingDir - The project working directory
127
+ * @param {Object} options - Generation options
128
+ * @param {string} [options.outputDir] - Output directory (default: .vizzly/report)
129
+ * @returns {Promise<{success: boolean, reportPath: string, error?: string}>}
130
+ */
131
+ export async function generateStaticReport(workingDir, options = {}) {
132
+ let outputDir = options.outputDir || join(workingDir, '.vizzly', 'report');
133
+ let vizzlyDir = join(workingDir, '.vizzly');
134
+ try {
135
+ // Read report data
136
+ let reportDataPath = join(vizzlyDir, 'report-data.json');
137
+ if (!existsSync(reportDataPath)) {
138
+ return {
139
+ success: false,
140
+ reportPath: null,
141
+ error: 'No report data found. Run tests first.'
142
+ };
143
+ }
144
+ let reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
145
+
146
+ // Read baseline metadata if available
147
+ let metadataPath = join(vizzlyDir, 'baselines', 'metadata.json');
148
+ if (existsSync(metadataPath)) {
149
+ try {
150
+ reportData.baseline = JSON.parse(readFileSync(metadataPath, 'utf8'));
151
+ } catch {
152
+ // Ignore metadata read errors
153
+ }
154
+ }
155
+
156
+ // Transform image URLs to relative paths
157
+ let transformedData = transformImageUrls(reportData);
158
+
159
+ // Load and use the SSR module
160
+ let ssrModulePath = getSSRModulePath();
161
+ let {
162
+ renderStaticReport
163
+ } = await import(ssrModulePath);
164
+ let renderedContent = renderStaticReport(transformedData);
165
+
166
+ // Get CSS
167
+ let reporterDistPath = getReporterDistPath();
168
+ let css = readFileSync(join(reporterDistPath, 'reporter-bundle.css'), 'utf8');
169
+
170
+ // Create output directory
171
+ mkdirSync(outputDir, {
172
+ recursive: true
173
+ });
174
+
175
+ // Copy image directories
176
+ let imageDirs = ['baselines', 'current', 'diffs'];
177
+ for (let dir of imageDirs) {
178
+ let srcDir = join(vizzlyDir, dir);
179
+ let destDir = join(outputDir, 'images', dir);
180
+ copyDirectory(srcDir, destDir);
181
+ }
182
+
183
+ // Generate and write HTML
184
+ let html = generateHtml(renderedContent, css);
185
+ let htmlPath = join(outputDir, 'index.html');
186
+ writeFileSync(htmlPath, html, 'utf8');
187
+ return {
188
+ success: true,
189
+ reportPath: htmlPath
190
+ };
191
+ } catch (error) {
192
+ return {
193
+ success: false,
194
+ reportPath: null,
195
+ error: error.message
196
+ };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get the file:// URL for a report path
202
+ */
203
+ export function getReportFileUrl(reportPath) {
204
+ return `file://${reportPath}`;
205
+ }
@@ -5,24 +5,44 @@
5
5
  import { execFile } from 'node:child_process';
6
6
  import { platform } from 'node:os';
7
7
 
8
+ /**
9
+ * Validate URL is safe to open (prevent command injection)
10
+ * Only allows http://, https://, and file:// URLs
11
+ * @param {string} url - URL to validate
12
+ * @returns {boolean} True if safe
13
+ */
14
+ function isValidUrl(url) {
15
+ if (typeof url !== 'string' || url.length === 0) {
16
+ return false;
17
+ }
18
+
19
+ // Only allow safe URL schemes
20
+ let validSchemes = ['http://', 'https://', 'file://'];
21
+ return validSchemes.some(scheme => url.startsWith(scheme));
22
+ }
23
+
8
24
  /**
9
25
  * Open a URL in the default browser
10
- * @param {string} url - URL to open
26
+ * @param {string} url - URL to open (must be http://, https://, or file://)
11
27
  * @returns {Promise<boolean>} True if successful
12
28
  */
13
29
  export async function openBrowser(url) {
30
+ // Validate URL to prevent command injection
31
+ if (!isValidUrl(url)) {
32
+ return false;
33
+ }
14
34
  return new Promise(resolve => {
15
35
  let command;
16
36
  let args;
17
- const os = platform();
18
- switch (os) {
37
+ let currentPlatform = platform();
38
+ switch (currentPlatform) {
19
39
  case 'darwin':
20
40
  // macOS
21
41
  command = 'open';
22
42
  args = [url];
23
43
  break;
24
44
  case 'win32':
25
- // Windows
45
+ // Windows - use start command with validated URL
26
46
  command = 'cmd.exe';
27
47
  args = ['/c', 'start', '""', url];
28
48
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -58,11 +58,12 @@
58
58
  ],
59
59
  "scripts": {
60
60
  "start": "node src/index.js",
61
- "build": "npm run clean && npm run compile && npm run build:reporter && npm run copy-types",
61
+ "build": "npm run clean && npm run compile && npm run build:reporter && npm run build:reporter-ssr && npm run copy-types",
62
62
  "clean": "rimraf dist",
63
63
  "compile": "babel src --out-dir dist --ignore '**/*.test.js'",
64
64
  "copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
65
65
  "build:reporter": "cd src/reporter && vite build",
66
+ "build:reporter-ssr": "cd src/reporter && vite build --config vite.ssr.config.js",
66
67
  "dev:reporter": "cd src/reporter && vite --config vite.dev.config.js",
67
68
  "test:types": "tsd",
68
69
  "prepublishOnly": "npm run build",