@vizzly-testing/cli 0.22.0 → 0.22.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.
package/dist/sdk/index.js CHANGED
@@ -247,6 +247,7 @@ export class VizzlySDK extends EventEmitter {
247
247
  buildId,
248
248
  name,
249
249
  image: imageBase64,
250
+ type: 'base64',
250
251
  properties: options.properties || {}
251
252
  };
252
253
 
@@ -47,7 +47,7 @@ export const createApiHandler = (client, {
47
47
  let vizzlyDisabled = false;
48
48
  let screenshotCount = 0;
49
49
  let uploadPromises = [];
50
- const handleScreenshot = async (buildId, name, image, properties = {}) => {
50
+ const handleScreenshot = async (buildId, name, image, properties = {}, type) => {
51
51
  if (vizzlyDisabled) {
52
52
  output.debug('upload', `${name} (disabled)`);
53
53
  return {
@@ -73,8 +73,11 @@ export const createApiHandler = (client, {
73
73
  }
74
74
 
75
75
  // Support both base64 encoded images and file paths
76
+ // Use explicit type from client if provided (fast path), otherwise detect (slow path)
77
+ // Only accept valid type values to prevent invalid types from bypassing detection
76
78
  let imageBuffer;
77
- const inputType = detectImageInputType(image);
79
+ let validTypes = ['base64', 'file-path'];
80
+ const inputType = type && validTypes.includes(type) ? type : detectImageInputType(image);
78
81
  if (inputType === 'file-path') {
79
82
  // It's a file path - resolve and read the file
80
83
  const filePath = resolve(image.replace('file://', ''));
@@ -267,7 +267,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
267
267
  output.debug('tdd', `baseline: ${baseline.buildName}`);
268
268
  }
269
269
  };
270
- const handleScreenshot = async (_buildId, name, image, properties = {}) => {
270
+ const handleScreenshot = async (_buildId, name, image, properties = {}, type) => {
271
271
  // Validate and sanitize screenshot name
272
272
  let sanitizedName;
273
273
  try {
@@ -306,8 +306,11 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
306
306
 
307
307
  // Support both base64 encoded images and file paths
308
308
  // Vitest browser mode returns file paths, so we need to handle both
309
+ // Use explicit type from client if provided (fast path), otherwise detect (slow path)
310
+ // Only accept valid type values to prevent invalid types from bypassing detection
309
311
  let imageBuffer;
310
- const inputType = detectImageInputType(image);
312
+ let validTypes = ['base64', 'file-path'];
313
+ const inputType = type && validTypes.includes(type) ? type : detectImageInputType(image);
311
314
  if (inputType === 'file-path') {
312
315
  // It's a file path - resolve and read the file
313
316
  const filePath = resolve(image.replace('file://', ''));
@@ -31,7 +31,8 @@ export function createScreenshotRouter({
31
31
  buildId,
32
32
  name,
33
33
  properties,
34
- image
34
+ image,
35
+ type
35
36
  } = body;
36
37
  if (!name || !image) {
37
38
  sendError(res, 400, 'name and image are required');
@@ -40,14 +41,15 @@ export function createScreenshotRouter({
40
41
 
41
42
  // Use buildId from request body, or fall back to server's buildId
42
43
  const effectiveBuildId = buildId || defaultBuildId;
43
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
44
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type);
44
45
  sendJson(res, result.statusCode, result.body);
45
46
  return true;
46
47
  } catch (error) {
47
48
  output.debug('Screenshot processing error:', {
48
- error: error.message
49
+ error: error.message,
50
+ stack: error.stack
49
51
  });
50
- sendError(res, 500, 'Failed to process screenshot');
52
+ sendError(res, 500, `Failed to process screenshot: ${error.message}`);
51
53
  return true;
52
54
  }
53
55
  }
@@ -11,7 +11,7 @@ import { platform } from 'node:os';
11
11
  * @param {string} url - URL to validate
12
12
  * @returns {boolean} True if safe
13
13
  */
14
- function isValidUrl(url) {
14
+ export function isValidBrowserUrl(url) {
15
15
  if (typeof url !== 'string' || url.length === 0) {
16
16
  return false;
17
17
  }
@@ -28,7 +28,7 @@ function isValidUrl(url) {
28
28
  */
29
29
  export async function openBrowser(url) {
30
30
  // Validate URL to prevent command injection
31
- if (!isValidUrl(url)) {
31
+ if (!isValidBrowserUrl(url)) {
32
32
  return false;
33
33
  }
34
34
  return new Promise(resolve => {
@@ -27,17 +27,32 @@ export function isBase64(str) {
27
27
  // Strip data URI prefix if present (e.g., data:image/png;base64,...)
28
28
  let base64Content = str;
29
29
  if (str.startsWith('data:')) {
30
- const match = str.match(/^data:[a-zA-Z0-9+/.-]+;base64,(.+)$/);
30
+ let match = str.match(/^data:[a-zA-Z0-9+/.-]+;base64,(.+)$/);
31
31
  if (!match) {
32
32
  return false; // Has data: prefix but invalid format
33
33
  }
34
34
  base64Content = match[1];
35
35
  }
36
36
 
37
- // Base64 regex: groups of 4 chars [A-Za-z0-9+/], with optional padding
38
- // Valid endings: no padding, or 2/3 chars + padding (= or ==)
39
- const base64Pattern = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
40
- return base64Pattern.test(base64Content);
37
+ // Quick check: base64 only contains these characters
38
+ // Use a simple character class check instead of a complex regex to avoid
39
+ // catastrophic backtracking on large strings
40
+ if (!/^[A-Za-z0-9+/=]+$/.test(base64Content)) {
41
+ return false;
42
+ }
43
+
44
+ // Check length is valid (must be multiple of 4, accounting for padding)
45
+ let len = base64Content.length;
46
+ if (len % 4 !== 0) {
47
+ return false;
48
+ }
49
+
50
+ // Check padding is valid (only at end, max 2 = chars)
51
+ let paddingMatch = base64Content.match(/=+$/);
52
+ if (paddingMatch && paddingMatch[0].length > 2) {
53
+ return false;
54
+ }
55
+ return true;
41
56
  }
42
57
 
43
58
  /**
@@ -66,41 +81,46 @@ export function looksLikeFilePath(str) {
66
81
  return false;
67
82
  }
68
83
 
69
- // 0. Explicitly reject data URIs first (they contain : and / which would match path patterns)
84
+ // 0. Length check - file paths are short, base64 screenshots are huge
85
+ // Even the longest realistic file path is < 500 chars
86
+ // This makes detection O(1) for large base64 strings
87
+ // Use same threshold (1000) as detectImageInputType for consistency
88
+ if (str.length > 1000) {
89
+ return false;
90
+ }
91
+
92
+ // 1. Explicitly reject data URIs (they contain : and / which would match path patterns)
70
93
  if (str.startsWith('data:')) {
71
94
  return false;
72
95
  }
73
96
 
74
- // 1. Check for file:// URI scheme
97
+ // 2. Check for file:// URI scheme
75
98
  if (str.startsWith('file://')) {
76
99
  return true;
77
100
  }
78
101
 
79
- // 2. Check for absolute paths (Unix or Windows)
80
- // Unix: starts with /
81
- // Windows: starts with drive letter like C:\ or C:/
82
- if (str.startsWith('/') || /^[A-Za-z]:[/\\]/.test(str)) {
102
+ // 3. Windows absolute paths (C:\ or C:/) - base64 never starts with drive letter
103
+ if (/^[A-Za-z]:[/\\]/.test(str)) {
83
104
  return true;
84
105
  }
85
106
 
86
- // 3. Check for relative path indicators
87
- // ./ or ../ or .\ or ..\
107
+ // 4. Relative path indicators (./ or ../) - base64 never starts with dot
88
108
  if (/^\.\.?[/\\]/.test(str)) {
89
109
  return true;
90
110
  }
91
111
 
92
- // 4. Check for path separators (forward or back slash)
93
- // This catches paths like: subdirectory/file.png or subdirectory\file.png
94
- if (/[/\\]/.test(str)) {
95
- return true;
96
- }
97
-
98
112
  // 5. Check for common image file extensions
99
- // This catches simple filenames like: screenshot.png
100
- // Common extensions: png, jpg, jpeg, gif, webp, bmp, svg, tiff, ico
113
+ // This is the safest check - base64 never ends with .png/.jpg/etc
114
+ // Catches: /path/file.png, subdir/file.png, file.png
101
115
  if (/\.(png|jpe?g|gif|webp|bmp|svg|tiff?|ico)$/i.test(str)) {
102
116
  return true;
103
117
  }
118
+
119
+ // Note: We intentionally don't check for bare "/" prefix or "/" anywhere
120
+ // because JPEG base64 starts with "/9j/" which would false-positive
121
+ // File paths without extensions are rare for images and will fall through
122
+ // to base64 detection, which is acceptable for backwards compat
123
+
104
124
  return false;
105
125
  }
106
126
 
@@ -112,14 +132,13 @@ export function looksLikeFilePath(str) {
112
132
  * - 'file-path': A file path (relative or absolute)
113
133
  * - 'unknown': Cannot determine (ambiguous or invalid)
114
134
  *
115
- * Strategy:
116
- * 1. First check if it's valid base64 (can contain / which might look like paths)
117
- * 2. Then check if it looks like a file path (more specific patterns)
118
- * 3. Otherwise return 'unknown'
135
+ * Strategy (optimized for performance):
136
+ * 1. Check for data URI prefix first (O(1), definitive)
137
+ * 2. Check file path patterns (O(1) prefix/suffix checks)
138
+ * 3. For large non-path strings, assume base64 (skip expensive validation)
139
+ * 4. Only run full base64 validation on small ambiguous strings
119
140
  *
120
- * This order prevents base64 strings (which can contain /) from being
121
- * misidentified as file paths. Base64 validation is stricter and should
122
- * be checked first.
141
+ * This avoids O(n) regex validation on large screenshot buffers.
123
142
  *
124
143
  * @param {string} str - String to detect
125
144
  * @returns {'base64' | 'file-path' | 'unknown'} Detected input type
@@ -136,15 +155,26 @@ export function detectImageInputType(str) {
136
155
  return 'unknown';
137
156
  }
138
157
 
139
- // Check base64 FIRST - base64 strings can contain / which looks like paths
140
- // Base64 validation is stricter and more deterministic
141
- if (isBase64(str)) {
158
+ // 1. Data URIs are definitively base64 (O(1) check)
159
+ if (str.startsWith('data:')) {
142
160
  return 'base64';
143
161
  }
144
162
 
145
- // Then check file path - catch patterns that aren't valid base64
163
+ // 2. Check file path patterns (O(1) prefix/suffix checks)
146
164
  if (looksLikeFilePath(str)) {
147
165
  return 'file-path';
148
166
  }
167
+
168
+ // 3. For large strings that aren't file paths, assume base64
169
+ // Screenshots are typically 100KB+ as base64, file paths are <1KB
170
+ // Skip expensive O(n) validation for large strings
171
+ if (str.length > 1000) {
172
+ return 'base64';
173
+ }
174
+
175
+ // 4. Full validation only for small ambiguous strings
176
+ if (isBase64(str)) {
177
+ return 'base64';
178
+ }
149
179
  return 'unknown';
150
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -87,7 +87,7 @@
87
87
  "registry": "https://registry.npmjs.org/"
88
88
  },
89
89
  "dependencies": {
90
- "@vizzly-testing/honeydiff": "^0.7.1",
90
+ "@vizzly-testing/honeydiff": "^0.8.0",
91
91
  "ansis": "^4.2.0",
92
92
  "commander": "^14.0.0",
93
93
  "cosmiconfig": "^9.0.0",