@vizzly-testing/cli 0.19.1 → 0.19.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/services/tdd-service.js +41 -2
- package/dist/utils/security.js +59 -2
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import crypto from 'node:crypto';
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { compare } from '@vizzly-testing/honeydiff';
|
|
27
27
|
import { NetworkError } from '../errors/vizzly-error.js';
|
|
@@ -184,11 +184,50 @@ export class TddService {
|
|
|
184
184
|
throw new Error(`Build ${buildId} not found or API returned null`);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// When downloading baselines, always start with a clean slate
|
|
188
|
+
// This handles signature property changes, build switches, and any stale state
|
|
189
|
+
output.info('Clearing local state before downloading baselines...');
|
|
190
|
+
try {
|
|
191
|
+
// Clear everything - baselines, current screenshots, diffs, and metadata
|
|
192
|
+
// This ensures we start fresh with the new baseline build
|
|
193
|
+
rmSync(this.baselinePath, {
|
|
194
|
+
recursive: true,
|
|
195
|
+
force: true
|
|
196
|
+
});
|
|
197
|
+
rmSync(this.currentPath, {
|
|
198
|
+
recursive: true,
|
|
199
|
+
force: true
|
|
200
|
+
});
|
|
201
|
+
rmSync(this.diffPath, {
|
|
202
|
+
recursive: true,
|
|
203
|
+
force: true
|
|
204
|
+
});
|
|
205
|
+
mkdirSync(this.baselinePath, {
|
|
206
|
+
recursive: true
|
|
207
|
+
});
|
|
208
|
+
mkdirSync(this.currentPath, {
|
|
209
|
+
recursive: true
|
|
210
|
+
});
|
|
211
|
+
mkdirSync(this.diffPath, {
|
|
212
|
+
recursive: true
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Clear baseline metadata file (will be regenerated with new baseline)
|
|
216
|
+
const baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
|
|
217
|
+
if (existsSync(baselineMetadataPath)) {
|
|
218
|
+
rmSync(baselineMetadataPath, {
|
|
219
|
+
force: true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
output.error(`Failed to clear local state: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
187
226
|
// Extract signature properties from API response (for variant support)
|
|
188
227
|
if (apiResponse.signatureProperties && Array.isArray(apiResponse.signatureProperties)) {
|
|
189
228
|
this.signatureProperties = apiResponse.signatureProperties;
|
|
190
229
|
if (this.signatureProperties.length > 0) {
|
|
191
|
-
output.info(`Using
|
|
230
|
+
output.info(`Using signature properties: ${this.signatureProperties.join(', ')}`);
|
|
192
231
|
}
|
|
193
232
|
}
|
|
194
233
|
baselineBuild = apiResponse.build;
|
package/dist/utils/security.js
CHANGED
|
@@ -13,6 +13,62 @@ import * as output from './output.js';
|
|
|
13
13
|
* @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
|
|
14
14
|
* @returns {string} Sanitized screenshot name
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Validate screenshot name for security (no transformations, just validation)
|
|
18
|
+
* Throws if name contains path traversal or other dangerous patterns
|
|
19
|
+
*
|
|
20
|
+
* @param {string} name - Screenshot name to validate
|
|
21
|
+
* @param {number} maxLength - Maximum allowed length
|
|
22
|
+
* @returns {string} The original name (unchanged) if valid
|
|
23
|
+
* @throws {Error} If name contains dangerous patterns
|
|
24
|
+
*/
|
|
25
|
+
export function validateScreenshotName(name, maxLength = 255) {
|
|
26
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
27
|
+
throw new Error('Screenshot name must be a non-empty string');
|
|
28
|
+
}
|
|
29
|
+
if (name.length > maxLength) {
|
|
30
|
+
throw new Error(`Screenshot name exceeds maximum length of ${maxLength} characters`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Block directory traversal patterns
|
|
34
|
+
if (name.includes('..') || name.includes('\\')) {
|
|
35
|
+
throw new Error('Screenshot name contains invalid path characters');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Block forward slashes (path separators)
|
|
39
|
+
if (name.includes('/')) {
|
|
40
|
+
throw new Error('Screenshot name cannot contain forward slashes');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Block absolute paths
|
|
44
|
+
if (isAbsolute(name)) {
|
|
45
|
+
throw new Error('Screenshot name cannot be an absolute path');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Return the original name unchanged - validation only!
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate screenshot name for security (allows spaces, preserves original name)
|
|
54
|
+
*
|
|
55
|
+
* This function only validates for security - it does NOT transform spaces.
|
|
56
|
+
* Spaces are preserved so that:
|
|
57
|
+
* 1. generateScreenshotSignature() uses the original name with spaces (matches cloud)
|
|
58
|
+
* 2. generateBaselineFilename() handles space→hyphen conversion (matches cloud)
|
|
59
|
+
*
|
|
60
|
+
* Flow: "VBtn dark" → sanitize → "VBtn dark" → signature: "VBtn dark|1265||" → filename: "VBtn-dark_hash.png"
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name - Screenshot name to validate
|
|
63
|
+
* @param {number} maxLength - Maximum allowed length (default: 255)
|
|
64
|
+
* @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
|
|
65
|
+
* @returns {string} The validated name (unchanged if valid, spaces preserved)
|
|
66
|
+
* @throws {Error} If name contains dangerous patterns
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* sanitizeScreenshotName("VBtn dark") // Returns "VBtn dark" (spaces preserved)
|
|
70
|
+
* sanitizeScreenshotName("My/Component") // Throws error (contains /)
|
|
71
|
+
*/
|
|
16
72
|
export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = false) {
|
|
17
73
|
if (typeof name !== 'string' || name.length === 0) {
|
|
18
74
|
throw new Error('Screenshot name must be a non-empty string');
|
|
@@ -36,9 +92,10 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
|
|
|
36
92
|
throw new Error('Screenshot name cannot be an absolute path');
|
|
37
93
|
}
|
|
38
94
|
|
|
39
|
-
// Allow only safe characters: alphanumeric, hyphens, underscores, dots, and optionally slashes
|
|
95
|
+
// Allow only safe characters: alphanumeric, hyphens, underscores, dots, spaces, and optionally slashes
|
|
96
|
+
// Spaces are allowed here and will be converted to hyphens in generateBaselineFilename() to match cloud behavior
|
|
40
97
|
// Replace other characters with underscores
|
|
41
|
-
const allowedChars = allowSlashes ? /[^a-zA-Z0-9._/-]/g : /[^a-zA-Z0-9._-]/g;
|
|
98
|
+
const allowedChars = allowSlashes ? /[^a-zA-Z0-9._ /-]/g : /[^a-zA-Z0-9._ -]/g;
|
|
42
99
|
let sanitized = name.replace(allowedChars, '_');
|
|
43
100
|
|
|
44
101
|
// Prevent names that start with dots (hidden files)
|