@vizzly-testing/cli 0.5.0 → 0.6.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.
- package/README.md +15 -9
- package/dist/commands/run.js +57 -18
- package/dist/commands/tdd.js +6 -13
- package/dist/server/handlers/tdd-handler.js +82 -8
- package/dist/services/html-report-generator.js +377 -0
- package/dist/services/report-generator/report.css +355 -0
- package/dist/services/report-generator/viewer.js +100 -0
- package/dist/services/server-manager.js +3 -2
- package/dist/services/tdd-service.js +375 -66
- package/dist/services/test-runner.js +54 -27
- package/dist/types/server/handlers/tdd-handler.d.ts +18 -1
- package/dist/types/services/html-report-generator.d.ts +52 -0
- package/dist/types/services/report-generator/viewer.d.ts +0 -0
- package/dist/types/services/server-manager.d.ts +19 -1
- package/dist/types/services/tdd-service.d.ts +24 -3
- package/dist/types/utils/config-loader.d.ts +3 -0
- package/dist/types/utils/security.d.ts +29 -0
- package/dist/utils/config-loader.js +7 -0
- package/dist/utils/security.js +154 -0
- package/docs/tdd-mode.md +58 -12
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -123,13 +123,19 @@ For local visual testing with immediate feedback, use the dedicated `tdd` comman
|
|
|
123
123
|
# First run - creates local baselines
|
|
124
124
|
vizzly tdd "npm test"
|
|
125
125
|
|
|
126
|
-
# Make changes and test - fails if visual differences detected
|
|
126
|
+
# Make changes and test - fails if visual differences detected
|
|
127
127
|
vizzly tdd "npm test"
|
|
128
128
|
|
|
129
129
|
# Accept changes as new baseline
|
|
130
130
|
vizzly tdd "npm test" --set-baseline
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
**Interactive HTML Report:** Each TDD run generates a detailed HTML report with visual comparison tools:
|
|
134
|
+
- **Overlay mode** - Toggle between baseline and current screenshots
|
|
135
|
+
- **Side-by-side mode** - Compare baseline and current images horizontally
|
|
136
|
+
- **Onion skin mode** - Drag to reveal differences interactively
|
|
137
|
+
- **Toggle mode** - Click to switch between baseline and current
|
|
138
|
+
|
|
133
139
|
**TDD Command Options:**
|
|
134
140
|
- `--set-baseline` - Accept current screenshots as new baseline
|
|
135
141
|
- `--baseline-build <id>` - Use specific build as baseline (requires API token)
|
|
@@ -203,25 +209,25 @@ export default {
|
|
|
203
209
|
// API configuration
|
|
204
210
|
// Set VIZZLY_TOKEN environment variable or uncomment and set here:
|
|
205
211
|
// apiToken: 'your-token-here',
|
|
206
|
-
|
|
212
|
+
|
|
207
213
|
// Screenshot configuration
|
|
208
214
|
screenshots: {
|
|
209
215
|
directory: './screenshots',
|
|
210
216
|
formats: ['png']
|
|
211
217
|
},
|
|
212
|
-
|
|
218
|
+
|
|
213
219
|
// Server configuration
|
|
214
220
|
server: {
|
|
215
221
|
port: 47392,
|
|
216
222
|
screenshotPath: '/screenshot'
|
|
217
223
|
},
|
|
218
|
-
|
|
224
|
+
|
|
219
225
|
// Comparison configuration
|
|
220
226
|
comparison: {
|
|
221
227
|
threshold: 0.1,
|
|
222
228
|
ignoreAntialiasing: true
|
|
223
229
|
},
|
|
224
|
-
|
|
230
|
+
|
|
225
231
|
// Upload configuration
|
|
226
232
|
upload: {
|
|
227
233
|
concurrency: 5,
|
|
@@ -278,8 +284,8 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
|
|
|
278
284
|
env:
|
|
279
285
|
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
280
286
|
# Optional: Provide correct git information from GitHub context
|
|
281
|
-
VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
282
|
-
VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
|
|
287
|
+
VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
|
|
288
|
+
VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
|
283
289
|
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
284
290
|
```
|
|
285
291
|
|
|
@@ -339,8 +345,8 @@ For enhanced CI/CD integration, you can override git detection with these enviro
|
|
|
339
345
|
**Example for GitHub Actions:**
|
|
340
346
|
```yaml
|
|
341
347
|
env:
|
|
342
|
-
VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
343
|
-
VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
|
|
348
|
+
VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
|
|
349
|
+
VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
|
344
350
|
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
345
351
|
VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
346
352
|
```
|
package/dist/commands/run.js
CHANGED
|
@@ -17,15 +17,28 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
17
17
|
color: !globalOptions.noColor
|
|
18
18
|
});
|
|
19
19
|
let testRunner = null;
|
|
20
|
-
let
|
|
20
|
+
let buildId = null;
|
|
21
|
+
let startTime = null;
|
|
22
|
+
let isTddMode = false;
|
|
21
23
|
|
|
22
24
|
// Ensure cleanup on exit
|
|
23
25
|
const cleanup = async () => {
|
|
24
26
|
ui.cleanup();
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
// Cancel test runner (kills process and stops server)
|
|
29
|
+
if (testRunner) {
|
|
26
30
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
await testRunner.cancel();
|
|
32
|
+
} catch {
|
|
33
|
+
// Silent fail
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Finalize build if we have one
|
|
38
|
+
if (testRunner && buildId) {
|
|
39
|
+
try {
|
|
40
|
+
const executionTime = Date.now() - (startTime || Date.now());
|
|
41
|
+
await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
|
|
29
42
|
} catch {
|
|
30
43
|
// Silent fail on cleanup
|
|
31
44
|
}
|
|
@@ -113,6 +126,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
113
126
|
});
|
|
114
127
|
testRunner.on('build-created', buildInfo => {
|
|
115
128
|
buildUrl = buildInfo.url;
|
|
129
|
+
buildId = buildInfo.buildId;
|
|
116
130
|
// Debug: Log build creation details
|
|
117
131
|
if (globalOptions.verbose) {
|
|
118
132
|
ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
|
|
@@ -153,21 +167,46 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
153
167
|
|
|
154
168
|
// Start test run
|
|
155
169
|
ui.info('Starting test execution...');
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
...runResult,
|
|
162
|
-
...result
|
|
163
|
-
};
|
|
164
|
-
ui.success('Test run completed successfully');
|
|
170
|
+
startTime = Date.now();
|
|
171
|
+
isTddMode = runOptions.tdd || false;
|
|
172
|
+
let result;
|
|
173
|
+
try {
|
|
174
|
+
result = await testRunner.run(runOptions);
|
|
165
175
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
// Store buildId for cleanup purposes
|
|
177
|
+
if (result.buildId) {
|
|
178
|
+
buildId = result.buildId;
|
|
179
|
+
}
|
|
180
|
+
ui.success('Test run completed successfully');
|
|
181
|
+
|
|
182
|
+
// Show Vizzly summary
|
|
183
|
+
if (result.buildId) {
|
|
184
|
+
console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
|
|
185
|
+
if (result.url) {
|
|
186
|
+
console.log(`🔗 Vizzly: View results at ${result.url}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Test execution failed - build should already be finalized by test runner
|
|
191
|
+
ui.stopSpinner();
|
|
192
|
+
|
|
193
|
+
// Check if it's a test command failure (as opposed to setup failure)
|
|
194
|
+
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
195
|
+
// Extract exit code from error message if available
|
|
196
|
+
const exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
197
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
198
|
+
ui.error('Test run failed');
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
exitCode
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
204
|
+
// Setup or other error
|
|
205
|
+
ui.error('Test run failed', error);
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
exitCode: 1
|
|
209
|
+
};
|
|
171
210
|
}
|
|
172
211
|
}
|
|
173
212
|
|
package/dist/commands/tdd.js
CHANGED
|
@@ -11,7 +11,6 @@ import { detectBranch, detectCommit } from '../utils/git.js';
|
|
|
11
11
|
* @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
|
|
12
12
|
*/
|
|
13
13
|
export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
|
|
14
|
-
// Create UI handler
|
|
15
14
|
const ui = new ConsoleUI({
|
|
16
15
|
json: globalOptions.json,
|
|
17
16
|
verbose: globalOptions.verbose,
|
|
@@ -43,11 +42,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
43
42
|
ui.warning('No API token detected - running in local-only mode');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
// Handle --set-baseline flag
|
|
47
|
-
if (options.setBaseline) {
|
|
48
|
-
ui.info('🐻 Baseline update mode - current screenshots will become new baselines');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
// Collect git metadata
|
|
52
46
|
const branch = await detectBranch(options.branch);
|
|
53
47
|
const commit = await detectCommit(options.commit);
|
|
@@ -117,19 +111,20 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
117
111
|
});
|
|
118
112
|
|
|
119
113
|
// Show informational messages about baseline behavior
|
|
120
|
-
if (
|
|
121
|
-
ui.info('
|
|
114
|
+
if (options.setBaseline) {
|
|
115
|
+
ui.info('🐻 Baseline update mode - will ignore existing baselines and create new ones');
|
|
116
|
+
} else if (config.baselineBuildId || config.baselineComparisonId) {
|
|
117
|
+
ui.info('API token available - will fetch remote baselines for local comparison');
|
|
118
|
+
} else if (config.apiKey) {
|
|
119
|
+
ui.info('API token available - will use existing local baselines or create new ones');
|
|
122
120
|
} else {
|
|
123
121
|
ui.warning('Running without API token - all screenshots will be marked as new');
|
|
124
122
|
}
|
|
125
|
-
|
|
126
|
-
// Prepare TDD run options (no uploads, local comparisons only)
|
|
127
123
|
const runOptions = {
|
|
128
124
|
testCommand,
|
|
129
125
|
port: config.server.port,
|
|
130
126
|
timeout: config.server.timeout,
|
|
131
127
|
tdd: true,
|
|
132
|
-
// Enable TDD mode
|
|
133
128
|
setBaseline: options.setBaseline || false,
|
|
134
129
|
// Pass through baseline update mode
|
|
135
130
|
branch,
|
|
@@ -142,8 +137,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
142
137
|
baselineComparisonId: config.baselineComparisonId,
|
|
143
138
|
wait: false // No build to wait for in TDD mode
|
|
144
139
|
};
|
|
145
|
-
|
|
146
|
-
// Start TDD test run (local comparisons only)
|
|
147
140
|
ui.info('Starting TDD test execution...');
|
|
148
141
|
const result = await testRunner.run(runOptions);
|
|
149
142
|
|
|
@@ -2,17 +2,32 @@ import { Buffer } from 'buffer';
|
|
|
2
2
|
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
3
3
|
import { TddService } from '../../services/tdd-service.js';
|
|
4
4
|
import { colors } from '../../utils/colors.js';
|
|
5
|
+
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
5
6
|
const logger = createServiceLogger('TDD-HANDLER');
|
|
6
|
-
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
|
|
7
|
-
const tddService = new TddService(config, workingDir);
|
|
7
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
|
|
8
|
+
const tddService = new TddService(config, workingDir, setBaseline);
|
|
8
9
|
const builds = new Map();
|
|
9
10
|
const initialize = async () => {
|
|
10
11
|
logger.info('🔄 TDD mode enabled - setting up local comparison...');
|
|
12
|
+
|
|
13
|
+
// In baseline update mode, skip all baseline loading/downloading
|
|
14
|
+
if (setBaseline) {
|
|
15
|
+
logger.info('📁 Ready for new baseline creation - all screenshots will be treated as new baselines');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if we have baseline override flags that should force a fresh download
|
|
20
|
+
const shouldForceDownload = (baselineBuild || baselineComparison) && config.apiKey;
|
|
21
|
+
if (shouldForceDownload) {
|
|
22
|
+
logger.info('📥 Baseline override specified, downloading fresh baselines from Vizzly...');
|
|
23
|
+
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
11
26
|
const baseline = await tddService.loadBaseline();
|
|
12
27
|
if (!baseline) {
|
|
13
28
|
if (config.apiKey) {
|
|
14
29
|
logger.info('📥 No local baseline found, downloading from Vizzly...');
|
|
15
|
-
await tddService.downloadBaselines(baselineBuild, baselineComparison);
|
|
30
|
+
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
16
31
|
} else {
|
|
17
32
|
logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
|
|
18
33
|
}
|
|
@@ -36,15 +51,72 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
36
51
|
if (!build) {
|
|
37
52
|
throw new Error(`Build ${buildId} not found`);
|
|
38
53
|
}
|
|
54
|
+
|
|
55
|
+
// Validate and sanitize screenshot name
|
|
56
|
+
let sanitizedName;
|
|
57
|
+
try {
|
|
58
|
+
sanitizedName = sanitizeScreenshotName(name);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
body: {
|
|
63
|
+
error: 'Invalid screenshot name',
|
|
64
|
+
details: error.message,
|
|
65
|
+
tddMode: true
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate and sanitize properties
|
|
71
|
+
let validatedProperties;
|
|
72
|
+
try {
|
|
73
|
+
validatedProperties = validateScreenshotProperties(properties);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
statusCode: 400,
|
|
77
|
+
body: {
|
|
78
|
+
error: 'Invalid screenshot properties',
|
|
79
|
+
details: error.message,
|
|
80
|
+
tddMode: true
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create unique screenshot name based on properties
|
|
86
|
+
let uniqueName = sanitizedName;
|
|
87
|
+
const relevantProps = [];
|
|
88
|
+
|
|
89
|
+
// Add browser to name if provided (already validated)
|
|
90
|
+
if (validatedProperties.browser) {
|
|
91
|
+
relevantProps.push(validatedProperties.browser);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add viewport info if provided (already validated)
|
|
95
|
+
if (validatedProperties.viewport && validatedProperties.viewport.width && validatedProperties.viewport.height) {
|
|
96
|
+
relevantProps.push(`${validatedProperties.viewport.width}x${validatedProperties.viewport.height}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Combine base name with relevant properties and sanitize the result
|
|
100
|
+
if (relevantProps.length > 0) {
|
|
101
|
+
let proposedUniqueName = `${sanitizedName}-${relevantProps.join('-')}`;
|
|
102
|
+
try {
|
|
103
|
+
uniqueName = sanitizeScreenshotName(proposedUniqueName);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// If the combined name is invalid, fall back to the base sanitized name
|
|
106
|
+
uniqueName = sanitizedName;
|
|
107
|
+
logger.warn(`Combined screenshot name invalid (${error.message}), using base name: ${uniqueName}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
39
110
|
const screenshot = {
|
|
40
|
-
name,
|
|
111
|
+
name: uniqueName,
|
|
112
|
+
originalName: name,
|
|
41
113
|
imageData: image,
|
|
42
|
-
properties,
|
|
114
|
+
properties: validatedProperties,
|
|
43
115
|
timestamp: Date.now()
|
|
44
116
|
};
|
|
45
117
|
build.screenshots.push(screenshot);
|
|
46
118
|
const imageBuffer = Buffer.from(image, 'base64');
|
|
47
|
-
const comparison = await tddService.compareScreenshot(
|
|
119
|
+
const comparison = await tddService.compareScreenshot(uniqueName, imageBuffer, validatedProperties);
|
|
48
120
|
if (comparison.status === 'failed') {
|
|
49
121
|
return {
|
|
50
122
|
statusCode: 422,
|
|
@@ -56,7 +128,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
56
128
|
status: comparison.status,
|
|
57
129
|
baseline: comparison.baseline,
|
|
58
130
|
current: comparison.current,
|
|
59
|
-
diff: comparison.diff
|
|
131
|
+
diff: comparison.diff,
|
|
132
|
+
diffPercentage: comparison.diffPercentage,
|
|
133
|
+
threshold: comparison.threshold
|
|
60
134
|
},
|
|
61
135
|
tddMode: true
|
|
62
136
|
}
|
|
@@ -112,7 +186,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
112
186
|
if (build.screenshots.length === 0) {
|
|
113
187
|
throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
|
|
114
188
|
}
|
|
115
|
-
const results = tddService.printResults();
|
|
189
|
+
const results = await tddService.printResults();
|
|
116
190
|
builds.delete(buildId);
|
|
117
191
|
return {
|
|
118
192
|
id: buildId,
|