@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/client/index.js +6 -3
- package/dist/reporter/reporter-bundle.iife.js +29 -29
- package/dist/sdk/index.js +1 -0
- package/dist/server/handlers/api-handler.js +5 -2
- package/dist/server/handlers/tdd-handler.js +5 -2
- package/dist/server/routers/screenshot.js +6 -4
- package/dist/utils/browser.js +2 -2
- package/dist/utils/image-input-detector.js +62 -32
- package/package.json +2 -2
package/dist/sdk/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
52
|
+
sendError(res, 500, `Failed to process screenshot: ${error.message}`);
|
|
51
53
|
return true;
|
|
52
54
|
}
|
|
53
55
|
}
|
package/dist/utils/browser.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
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
|
-
//
|
|
97
|
+
// 2. Check for file:// URI scheme
|
|
75
98
|
if (str.startsWith('file://')) {
|
|
76
99
|
return true;
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
//
|
|
80
|
-
|
|
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
|
-
//
|
|
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
|
|
100
|
-
//
|
|
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.
|
|
117
|
-
* 2.
|
|
118
|
-
* 3.
|
|
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
|
|
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
|
-
//
|
|
140
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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",
|