@vizzly-testing/cli 0.4.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 +35 -5
- package/dist/cli.js +17 -1
- package/dist/commands/run.js +62 -21
- package/dist/commands/tdd.js +35 -35
- package/dist/commands/upload.js +4 -2
- 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 +64 -35
- package/dist/services/uploader.js +5 -3
- package/dist/types/commands/tdd.d.ts +3 -2
- 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/services/uploader.d.ts +2 -1
- package/dist/types/utils/ci-env.d.ts +55 -0
- package/dist/types/utils/config-loader.d.ts +3 -0
- package/dist/types/utils/git.d.ts +12 -0
- package/dist/types/utils/security.d.ts +29 -0
- package/dist/utils/ci-env.js +293 -0
- package/dist/utils/config-loader.js +7 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/security.js +154 -0
- package/docs/api-reference.md +15 -0
- package/docs/tdd-mode.md +58 -12
- package/docs/test-integration.md +6 -0
- package/package.json +6 -6
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,
|
|
@@ -277,6 +283,10 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
|
|
|
277
283
|
run: npx vizzly run "npm test" --wait
|
|
278
284
|
env:
|
|
279
285
|
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
286
|
+
# Optional: Provide correct git information from GitHub context
|
|
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 }}
|
|
289
|
+
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
280
290
|
```
|
|
281
291
|
|
|
282
292
|
### GitLab CI
|
|
@@ -319,10 +329,30 @@ Check if Vizzly is enabled in the current environment.
|
|
|
319
329
|
|
|
320
330
|
## Environment Variables
|
|
321
331
|
|
|
332
|
+
### Core Configuration
|
|
322
333
|
- `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
|
|
323
334
|
- `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
|
|
324
335
|
- `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
|
|
325
336
|
|
|
337
|
+
### Git Information Override
|
|
338
|
+
For enhanced CI/CD integration, you can override git detection with these environment variables:
|
|
339
|
+
|
|
340
|
+
- `VIZZLY_COMMIT_SHA`: Override detected commit SHA. Useful in CI environments.
|
|
341
|
+
- `VIZZLY_COMMIT_MESSAGE`: Override detected commit message. Useful in CI environments.
|
|
342
|
+
- `VIZZLY_BRANCH`: Override detected branch name. Useful in CI environments.
|
|
343
|
+
- `VIZZLY_PR_NUMBER`: Override detected pull request number. Useful for PR-specific builds.
|
|
344
|
+
|
|
345
|
+
**Example for GitHub Actions:**
|
|
346
|
+
```yaml
|
|
347
|
+
env:
|
|
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 }}
|
|
350
|
+
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
351
|
+
VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
These variables take highest priority over both CLI arguments and automatic git detection.
|
|
355
|
+
|
|
326
356
|
## Contributing
|
|
327
357
|
|
|
328
358
|
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes Vizzly better for everyone.
|
package/dist/cli.js
CHANGED
|
@@ -38,10 +38,26 @@ program.command('tdd').description('Run tests in TDD mode with local visual comp
|
|
|
38
38
|
validationErrors.forEach(error => console.error(` - ${error}`));
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const {
|
|
42
|
+
result,
|
|
43
|
+
cleanup
|
|
44
|
+
} = await tddCommand(command, options, globalOptions);
|
|
45
|
+
|
|
46
|
+
// Set up cleanup on process signals
|
|
47
|
+
const handleCleanup = async () => {
|
|
48
|
+
await cleanup();
|
|
49
|
+
};
|
|
50
|
+
process.once('SIGINT', () => {
|
|
51
|
+
handleCleanup().then(() => process.exit(1));
|
|
52
|
+
});
|
|
53
|
+
process.once('SIGTERM', () => {
|
|
54
|
+
handleCleanup().then(() => process.exit(1));
|
|
55
|
+
});
|
|
42
56
|
if (result && !result.success && result.exitCode > 0) {
|
|
57
|
+
await cleanup();
|
|
43
58
|
process.exit(result.exitCode);
|
|
44
59
|
}
|
|
60
|
+
await cleanup();
|
|
45
61
|
});
|
|
46
62
|
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (command, options) => {
|
|
47
63
|
const globalOptions = program.opts();
|
package/dist/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from '../utils/config-loader.js';
|
|
2
2
|
import { ConsoleUI } from '../utils/console-ui.js';
|
|
3
3
|
import { createServiceContainer } from '../container/index.js';
|
|
4
|
-
import { detectBranch, detectCommit,
|
|
4
|
+
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Run command implementation
|
|
@@ -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) {
|
|
30
|
+
try {
|
|
31
|
+
await testRunner.cancel();
|
|
32
|
+
} catch {
|
|
33
|
+
// Silent fail
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Finalize build if we have one
|
|
38
|
+
if (testRunner && buildId) {
|
|
26
39
|
try {
|
|
27
|
-
|
|
28
|
-
await testRunner.finalizeBuild(
|
|
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
|
}
|
|
@@ -55,8 +68,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
55
68
|
// Collect git metadata and build info
|
|
56
69
|
const branch = await detectBranch(options.branch);
|
|
57
70
|
const commit = await detectCommit(options.commit);
|
|
58
|
-
const message = options.message || (await
|
|
71
|
+
const message = options.message || (await detectCommitMessage());
|
|
59
72
|
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
73
|
+
const pullRequestNumber = detectPullRequestNumber();
|
|
60
74
|
if (globalOptions.verbose) {
|
|
61
75
|
ui.info('Configuration loaded', {
|
|
62
76
|
testCommand,
|
|
@@ -112,6 +126,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
126
|
});
|
|
113
127
|
testRunner.on('build-created', buildInfo => {
|
|
114
128
|
buildUrl = buildInfo.url;
|
|
129
|
+
buildId = buildInfo.buildId;
|
|
115
130
|
// Debug: Log build creation details
|
|
116
131
|
if (globalOptions.verbose) {
|
|
117
132
|
ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
|
|
@@ -146,26 +161,52 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
146
161
|
eager: config.eager || false,
|
|
147
162
|
allowNoToken: config.allowNoToken || false,
|
|
148
163
|
wait: config.wait || options.wait || false,
|
|
149
|
-
uploadAll: options.uploadAll || false
|
|
164
|
+
uploadAll: options.uploadAll || false,
|
|
165
|
+
pullRequestNumber
|
|
150
166
|
};
|
|
151
167
|
|
|
152
168
|
// Start test run
|
|
153
169
|
ui.info('Starting test execution...');
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
...runResult,
|
|
160
|
-
...result
|
|
161
|
-
};
|
|
162
|
-
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);
|
|
163
175
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
};
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
|
package/dist/commands/tdd.js
CHANGED
|
@@ -8,29 +8,26 @@ import { detectBranch, detectCommit } from '../utils/git.js';
|
|
|
8
8
|
* @param {string} testCommand - Test command to execute
|
|
9
9
|
* @param {Object} options - Command options
|
|
10
10
|
* @param {Object} globalOptions - Global CLI options
|
|
11
|
+
* @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
|
|
11
12
|
*/
|
|
12
13
|
export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
|
|
13
|
-
// Create UI handler
|
|
14
14
|
const ui = new ConsoleUI({
|
|
15
15
|
json: globalOptions.json,
|
|
16
16
|
verbose: globalOptions.verbose,
|
|
17
17
|
color: !globalOptions.noColor
|
|
18
18
|
});
|
|
19
19
|
let testRunner = null;
|
|
20
|
+
let isCleanedUp = false;
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
+
// Create cleanup function that can be called by the caller
|
|
22
23
|
const cleanup = async () => {
|
|
24
|
+
if (isCleanedUp) return;
|
|
25
|
+
isCleanedUp = true;
|
|
23
26
|
ui.cleanup();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const sigintHandler = async () => {
|
|
28
|
-
await cleanup();
|
|
29
|
-
process.exit(1);
|
|
27
|
+
if (testRunner?.cancel) {
|
|
28
|
+
await testRunner.cancel();
|
|
29
|
+
}
|
|
30
30
|
};
|
|
31
|
-
const exitHandler = () => ui.cleanup();
|
|
32
|
-
process.on('SIGINT', sigintHandler);
|
|
33
|
-
process.on('exit', exitHandler);
|
|
34
31
|
try {
|
|
35
32
|
// Load configuration with CLI overrides
|
|
36
33
|
const allOptions = {
|
|
@@ -45,11 +42,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
45
42
|
ui.warning('No API token detected - running in local-only mode');
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
// Handle --set-baseline flag
|
|
49
|
-
if (options.setBaseline) {
|
|
50
|
-
ui.info('🐻 Baseline update mode - current screenshots will become new baselines');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
45
|
// Collect git metadata
|
|
54
46
|
const branch = await detectBranch(options.branch);
|
|
55
47
|
const commit = await detectCommit(options.commit);
|
|
@@ -119,19 +111,20 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
119
111
|
});
|
|
120
112
|
|
|
121
113
|
// Show informational messages about baseline behavior
|
|
122
|
-
if (
|
|
123
|
-
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');
|
|
124
120
|
} else {
|
|
125
121
|
ui.warning('Running without API token - all screenshots will be marked as new');
|
|
126
122
|
}
|
|
127
|
-
|
|
128
|
-
// Prepare TDD run options (no uploads, local comparisons only)
|
|
129
123
|
const runOptions = {
|
|
130
124
|
testCommand,
|
|
131
125
|
port: config.server.port,
|
|
132
126
|
timeout: config.server.timeout,
|
|
133
127
|
tdd: true,
|
|
134
|
-
// Enable TDD mode
|
|
135
128
|
setBaseline: options.setBaseline || false,
|
|
136
129
|
// Pass through baseline update mode
|
|
137
130
|
branch,
|
|
@@ -144,8 +137,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
144
137
|
baselineComparisonId: config.baselineComparisonId,
|
|
145
138
|
wait: false // No build to wait for in TDD mode
|
|
146
139
|
};
|
|
147
|
-
|
|
148
|
-
// Start TDD test run (local comparisons only)
|
|
149
140
|
ui.info('Starting TDD test execution...');
|
|
150
141
|
const result = await testRunner.run(runOptions);
|
|
151
142
|
|
|
@@ -166,22 +157,31 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
166
157
|
}
|
|
167
158
|
ui.success('TDD test run completed');
|
|
168
159
|
|
|
169
|
-
//
|
|
170
|
-
|
|
160
|
+
// Determine success based on comparison results
|
|
161
|
+
const hasFailures = result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed');
|
|
162
|
+
if (hasFailures) {
|
|
171
163
|
ui.error('Visual differences detected in TDD mode', {}, 0);
|
|
172
|
-
// Return error status without calling process.exit in tests
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
exitCode: 1
|
|
176
|
-
};
|
|
177
164
|
}
|
|
178
|
-
|
|
165
|
+
|
|
166
|
+
// Return result and cleanup function
|
|
167
|
+
return {
|
|
168
|
+
result: {
|
|
169
|
+
success: !hasFailures,
|
|
170
|
+
exitCode: hasFailures ? 1 : 0,
|
|
171
|
+
...result
|
|
172
|
+
},
|
|
173
|
+
cleanup
|
|
174
|
+
};
|
|
179
175
|
} catch (error) {
|
|
180
176
|
ui.error('TDD test run failed', error);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
return {
|
|
178
|
+
result: {
|
|
179
|
+
success: false,
|
|
180
|
+
exitCode: 1,
|
|
181
|
+
error: error.message
|
|
182
|
+
},
|
|
183
|
+
cleanup
|
|
184
|
+
};
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from '../utils/config-loader.js';
|
|
2
2
|
import { ConsoleUI } from '../utils/console-ui.js';
|
|
3
3
|
import { createServiceContainer } from '../container/index.js';
|
|
4
|
-
import { detectBranch, detectCommit,
|
|
4
|
+
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
5
5
|
import { ApiService } from '../services/api-service.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -71,8 +71,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
71
71
|
// Collect git metadata if not provided
|
|
72
72
|
const branch = await detectBranch(options.branch);
|
|
73
73
|
const commit = await detectCommit(options.commit);
|
|
74
|
-
const message = options.message || (await
|
|
74
|
+
const message = options.message || (await detectCommitMessage());
|
|
75
75
|
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
76
|
+
const pullRequestNumber = detectPullRequestNumber();
|
|
76
77
|
ui.info(`Uploading screenshots from: ${screenshotsPath}`);
|
|
77
78
|
if (globalOptions.verbose) {
|
|
78
79
|
ui.info('Configuration loaded', {
|
|
@@ -99,6 +100,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
99
100
|
threshold: config.comparison.threshold,
|
|
100
101
|
uploadAll: options.uploadAll || false,
|
|
101
102
|
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
103
|
+
pullRequestNumber,
|
|
102
104
|
onProgress: progressData => {
|
|
103
105
|
const {
|
|
104
106
|
message: progressMessage,
|
|
@@ -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,
|