@vizzly-testing/cli 0.5.0 → 0.7.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.
Files changed (33) hide show
  1. package/README.md +55 -9
  2. package/dist/cli.js +15 -2
  3. package/dist/commands/finalize.js +72 -0
  4. package/dist/commands/run.js +59 -19
  5. package/dist/commands/tdd.js +6 -13
  6. package/dist/commands/upload.js +1 -0
  7. package/dist/server/handlers/tdd-handler.js +82 -8
  8. package/dist/services/api-service.js +14 -0
  9. package/dist/services/html-report-generator.js +377 -0
  10. package/dist/services/report-generator/report.css +355 -0
  11. package/dist/services/report-generator/viewer.js +100 -0
  12. package/dist/services/server-manager.js +3 -2
  13. package/dist/services/tdd-service.js +436 -66
  14. package/dist/services/test-runner.js +56 -28
  15. package/dist/services/uploader.js +3 -2
  16. package/dist/types/commands/finalize.d.ts +13 -0
  17. package/dist/types/server/handlers/tdd-handler.d.ts +18 -1
  18. package/dist/types/services/api-service.d.ts +6 -0
  19. package/dist/types/services/html-report-generator.d.ts +52 -0
  20. package/dist/types/services/report-generator/viewer.d.ts +0 -0
  21. package/dist/types/services/server-manager.d.ts +19 -1
  22. package/dist/types/services/tdd-service.d.ts +24 -3
  23. package/dist/types/services/uploader.d.ts +2 -1
  24. package/dist/types/utils/config-loader.d.ts +3 -0
  25. package/dist/types/utils/environment-config.d.ts +5 -0
  26. package/dist/types/utils/security.d.ts +29 -0
  27. package/dist/utils/config-loader.js +11 -1
  28. package/dist/utils/environment-config.js +9 -0
  29. package/dist/utils/security.js +154 -0
  30. package/docs/api-reference.md +27 -0
  31. package/docs/tdd-mode.md +58 -12
  32. package/docs/test-integration.md +69 -0
  33. package/package.json +3 -2
@@ -39,10 +39,11 @@ export class TestRunner extends BaseService {
39
39
  screenshotsCaptured: 0
40
40
  };
41
41
  }
42
+ let buildUrl = null;
43
+ let screenshotCount = 0;
44
+ let testSuccess = false;
45
+ let testError = null;
42
46
  try {
43
- let buildUrl = null;
44
- let screenshotCount = 0;
45
-
46
47
  // Create build based on mode
47
48
  buildId = await this.createBuild(options, tdd);
48
49
  if (!tdd && buildId) {
@@ -62,7 +63,7 @@ export class TestRunner extends BaseService {
62
63
  }
63
64
 
64
65
  // Start server with appropriate handler
65
- await this.serverManager.start(buildId, tdd);
66
+ await this.serverManager.start(buildId, tdd, options.setBaseline);
66
67
 
67
68
  // Forward server events
68
69
  if (this.serverManager.server?.emitter) {
@@ -78,28 +79,46 @@ export class TestRunner extends BaseService {
78
79
  VIZZLY_ENABLED: 'true',
79
80
  VIZZLY_SET_BASELINE: options.setBaseline || options['set-baseline'] ? 'true' : 'false'
80
81
  };
81
- await this.executeTestCommand(testCommand, env);
82
-
83
- // Finalize build
84
- const executionTime = Date.now() - startTime;
85
- await this.finalizeBuild(buildId, tdd, true, executionTime);
86
- return {
87
- buildId: buildId,
88
- url: buildUrl,
89
- testsPassed: 1,
90
- testsFailed: 0,
91
- screenshotsCaptured: screenshotCount
92
- };
82
+ try {
83
+ await this.executeTestCommand(testCommand, env);
84
+ testSuccess = true;
85
+ } catch (error) {
86
+ testError = error;
87
+ testSuccess = false;
88
+ }
93
89
  } catch (error) {
94
- this.logger.error('Test run failed:', error);
95
-
96
- // Finalize build on failure
97
- const executionTime = Date.now() - startTime;
98
- await this.finalizeBuild(buildId, tdd, false, executionTime);
99
- throw error;
90
+ // Error in setup phase
91
+ testError = error;
92
+ testSuccess = false;
100
93
  } finally {
101
- await this.serverManager.stop();
94
+ // Always finalize the build and stop the server
95
+ const executionTime = Date.now() - startTime;
96
+ if (buildId) {
97
+ try {
98
+ await this.finalizeBuild(buildId, tdd, testSuccess, executionTime);
99
+ } catch (finalizeError) {
100
+ this.logger.error('Failed to finalize build:', finalizeError);
101
+ }
102
+ }
103
+ try {
104
+ await this.serverManager.stop();
105
+ } catch (stopError) {
106
+ this.logger.error('Failed to stop server:', stopError);
107
+ }
102
108
  }
109
+
110
+ // If there was a test error, throw it now (after cleanup)
111
+ if (testError) {
112
+ this.logger.error('Test run failed:', testError);
113
+ throw testError;
114
+ }
115
+ return {
116
+ buildId: buildId,
117
+ url: buildUrl,
118
+ testsPassed: testSuccess ? 1 : 0,
119
+ testsFailed: testSuccess ? 0 : 1,
120
+ screenshotsCaptured: screenshotCount
121
+ };
103
122
  }
104
123
  async createBuild(options, tdd) {
105
124
  if (tdd) {
@@ -119,7 +138,8 @@ export class TestRunner extends BaseService {
119
138
  environment: options.environment || 'test',
120
139
  commit_sha: options.commit,
121
140
  commit_message: options.message,
122
- github_pull_request_number: options.pullRequestNumber
141
+ github_pull_request_number: options.pullRequestNumber,
142
+ parallel_id: options.parallelId
123
143
  });
124
144
  this.logger.debug(`Build created with ID: ${buildResult.id}`);
125
145
 
@@ -190,8 +210,11 @@ export class TestRunner extends BaseService {
190
210
  this.testProcess.on('error', error => {
191
211
  reject(new VizzlyError(`Failed to run test command: ${error.message}`), 'TEST_COMMAND_FAILED');
192
212
  });
193
- this.testProcess.on('exit', code => {
194
- if (code !== 0) {
213
+ this.testProcess.on('exit', (code, signal) => {
214
+ // If process was killed by SIGINT, treat as interruption
215
+ if (signal === 'SIGINT') {
216
+ reject(new VizzlyError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
217
+ } else if (code !== 0) {
195
218
  reject(new VizzlyError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
196
219
  } else {
197
220
  resolve();
@@ -200,8 +223,13 @@ export class TestRunner extends BaseService {
200
223
  });
201
224
  }
202
225
  async cancel() {
203
- if (this.testProcess) {
204
- this.testProcess.kill('SIGTERM');
226
+ if (this.testProcess && !this.testProcess.killed) {
227
+ this.testProcess.kill('SIGKILL');
228
+ }
229
+
230
+ // Stop server manager if running
231
+ if (this.serverManager) {
232
+ await this.serverManager.stop();
205
233
  }
206
234
  }
207
235
  }
@@ -51,6 +51,7 @@ export function createUploader({
51
51
  environment = 'production',
52
52
  threshold,
53
53
  pullRequestNumber,
54
+ parallelId,
54
55
  onProgress = () => {}
55
56
  }) {
56
57
  try {
@@ -101,7 +102,8 @@ export function createUploader({
101
102
  commit_message: message,
102
103
  environment,
103
104
  threshold,
104
- github_pull_request_number: pullRequestNumber
105
+ github_pull_request_number: pullRequestNumber,
106
+ parallel_id: parallelId
105
107
  };
106
108
  const build = await api.createBuild(buildInfo);
107
109
  const buildId = build.id;
@@ -219,7 +221,6 @@ export function createUploader({
219
221
  if (build.status === 'failed') {
220
222
  throw new UploadError(`Build failed: ${build.error || 'Unknown error'}`);
221
223
  }
222
- await new Promise(resolve => setTimeout(resolve, 2000));
223
224
  }
224
225
  throw new TimeoutError(`Build timed out after ${timeout}ms`, {
225
226
  buildId,
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Finalize command implementation
3
+ * @param {string} parallelId - Parallel ID to finalize
4
+ * @param {Object} options - Command options
5
+ * @param {Object} globalOptions - Global CLI options
6
+ */
7
+ export function finalizeCommand(parallelId: string, options?: any, globalOptions?: any): Promise<void>;
8
+ /**
9
+ * Validate finalize options
10
+ * @param {string} parallelId - Parallel ID to finalize
11
+ * @param {Object} options - Command options
12
+ */
13
+ export function validateFinalizeOptions(parallelId: string, _options: any): string[];
@@ -1,7 +1,18 @@
1
- export function createTddHandler(config: any, workingDir: any, baselineBuild: any, baselineComparison: any): {
1
+ export function createTddHandler(config: any, workingDir: any, baselineBuild: any, baselineComparison: any, setBaseline?: boolean): {
2
2
  initialize: () => Promise<void>;
3
3
  registerBuild: (buildId: any) => void;
4
4
  handleScreenshot: (buildId: any, name: any, image: any, properties?: {}) => Promise<{
5
+ statusCode: number;
6
+ body: {
7
+ error: string;
8
+ details: any;
9
+ tddMode: boolean;
10
+ comparison?: undefined;
11
+ status?: undefined;
12
+ message?: undefined;
13
+ success?: undefined;
14
+ };
15
+ } | {
5
16
  statusCode: number;
6
17
  body: {
7
18
  error: string;
@@ -12,6 +23,8 @@ export function createTddHandler(config: any, workingDir: any, baselineBuild: an
12
23
  baseline: any;
13
24
  current: any;
14
25
  diff: any;
26
+ diffPercentage: any;
27
+ threshold: any;
15
28
  };
16
29
  tddMode: boolean;
17
30
  status?: undefined;
@@ -29,6 +42,8 @@ export function createTddHandler(config: any, workingDir: any, baselineBuild: an
29
42
  baseline: any;
30
43
  current: any;
31
44
  diff?: undefined;
45
+ diffPercentage?: undefined;
46
+ threshold?: undefined;
32
47
  };
33
48
  tddMode: boolean;
34
49
  error?: undefined;
@@ -56,6 +71,8 @@ export function createTddHandler(config: any, workingDir: any, baselineBuild: an
56
71
  baseline?: undefined;
57
72
  current?: undefined;
58
73
  diff?: undefined;
74
+ diffPercentage?: undefined;
75
+ threshold?: undefined;
59
76
  };
60
77
  tddMode: boolean;
61
78
  error?: undefined;
@@ -76,4 +76,10 @@ export class ApiService {
76
76
  * @returns {Promise<Object>} Token context data
77
77
  */
78
78
  getTokenContext(): Promise<any>;
79
+ /**
80
+ * Finalize a parallel build
81
+ * @param {string} parallelId - Parallel ID to finalize
82
+ * @returns {Promise<Object>} Finalization result
83
+ */
84
+ finalizeParallelBuild(parallelId: string): Promise<any>;
79
85
  }
@@ -0,0 +1,52 @@
1
+ export class HtmlReportGenerator {
2
+ constructor(workingDir: any, config: any);
3
+ workingDir: any;
4
+ config: any;
5
+ reportDir: string;
6
+ reportPath: string;
7
+ cssPath: string;
8
+ /**
9
+ * Sanitize HTML content to prevent XSS attacks
10
+ * @param {string} text - Text to sanitize
11
+ * @returns {string} Sanitized text
12
+ */
13
+ sanitizeHtml(text: string): string;
14
+ /**
15
+ * Sanitize build info object
16
+ * @param {Object} buildInfo - Build information to sanitize
17
+ * @returns {Object} Sanitized build info
18
+ */
19
+ sanitizeBuildInfo(buildInfo?: any): any;
20
+ /**
21
+ * Generate HTML report from TDD results
22
+ * @param {Object} results - TDD comparison results
23
+ * @param {Object} buildInfo - Build information
24
+ * @returns {string} Path to generated report
25
+ */
26
+ generateReport(results: any, buildInfo?: any): string;
27
+ /**
28
+ * Process comparison data for HTML report
29
+ * @param {Object} comparison - Comparison object
30
+ * @returns {Object} Processed comparison data
31
+ */
32
+ processComparison(comparison: any): any;
33
+ /**
34
+ * Get relative path from report directory to image file
35
+ * @param {string} imagePath - Absolute path to image
36
+ * @param {string} reportDir - Report directory path
37
+ * @returns {string|null} Relative path or null if invalid
38
+ */
39
+ getRelativePath(imagePath: string, reportDir: string): string | null;
40
+ /**
41
+ * Generate the complete HTML template
42
+ * @param {Object} data - Report data
43
+ * @returns {string} HTML content
44
+ */
45
+ generateHtmlTemplate(data: any): string;
46
+ /**
47
+ * Generate HTML for a single comparison
48
+ * @param {Object} comparison - Comparison data
49
+ * @returns {string} HTML content
50
+ */
51
+ generateComparisonHtml(comparison: any): string;
52
+ }
@@ -9,6 +9,17 @@ export class ServerManager extends BaseService {
9
9
  initialize: () => Promise<void>;
10
10
  registerBuild: (buildId: any) => void;
11
11
  handleScreenshot: (buildId: any, name: any, image: any, properties?: {}) => Promise<{
12
+ statusCode: number;
13
+ body: {
14
+ error: string;
15
+ details: any;
16
+ tddMode: boolean;
17
+ comparison?: undefined;
18
+ status?: undefined;
19
+ message?: undefined;
20
+ success?: undefined;
21
+ };
22
+ } | {
12
23
  statusCode: number;
13
24
  body: {
14
25
  error: string;
@@ -19,6 +30,8 @@ export class ServerManager extends BaseService {
19
30
  baseline: any;
20
31
  current: any;
21
32
  diff: any;
33
+ diffPercentage: any;
34
+ threshold: any;
22
35
  };
23
36
  tddMode: boolean;
24
37
  status?: undefined;
@@ -36,6 +49,8 @@ export class ServerManager extends BaseService {
36
49
  baseline: any;
37
50
  current: any;
38
51
  diff?: undefined;
52
+ diffPercentage?: undefined;
53
+ threshold?: undefined;
39
54
  };
40
55
  tddMode: boolean;
41
56
  error?: undefined;
@@ -63,6 +78,8 @@ export class ServerManager extends BaseService {
63
78
  baseline?: undefined;
64
79
  current?: undefined;
65
80
  diff?: undefined;
81
+ diffPercentage?: undefined;
82
+ threshold?: undefined;
66
83
  };
67
84
  tddMode: boolean;
68
85
  error?: undefined;
@@ -139,9 +156,10 @@ export class ServerManager extends BaseService {
139
156
  cleanup: () => void;
140
157
  };
141
158
  emitter: EventEmitter<[never]>;
142
- start(buildId?: any, tddMode?: boolean): Promise<void>;
159
+ start(buildId?: any, tddMode?: boolean, setBaseline?: boolean): Promise<void>;
143
160
  buildId: any;
144
161
  tddMode: boolean;
162
+ setBaseline: boolean;
145
163
  createApiService(): Promise<import("./api-service.js").ApiService>;
146
164
  get server(): {
147
165
  emitter: EventEmitter<[never]>;
@@ -3,8 +3,9 @@
3
3
  */
4
4
  export function createTDDService(config: any, options?: {}): TddService;
5
5
  export class TddService {
6
- constructor(config: any, workingDir?: string);
6
+ constructor(config: any, workingDir?: string, setBaseline?: boolean);
7
7
  config: any;
8
+ setBaseline: boolean;
8
9
  api: ApiService;
9
10
  workingDir: string;
10
11
  baselinePath: string;
@@ -14,6 +15,11 @@ export class TddService {
14
15
  comparisons: any[];
15
16
  threshold: any;
16
17
  downloadBaselines(environment?: string, branch?: any, buildId?: any, comparisonId?: any): Promise<any>;
18
+ /**
19
+ * Handle local baseline logic (either load existing or prepare for new baselines)
20
+ * @returns {Promise<Object|null>} Baseline data or null if no local baselines exist
21
+ */
22
+ handleLocalBaselines(): Promise<any | null>;
17
23
  loadBaseline(): Promise<any>;
18
24
  compareScreenshot(name: any, imageBuffer: any, properties?: {}): Promise<{
19
25
  name: any;
@@ -32,7 +38,7 @@ export class TddService {
32
38
  comparisons: any[];
33
39
  baseline: any;
34
40
  };
35
- printResults(): {
41
+ printResults(): Promise<{
36
42
  total: number;
37
43
  passed: number;
38
44
  failed: number;
@@ -40,12 +46,27 @@ export class TddService {
40
46
  errors: number;
41
47
  comparisons: any[];
42
48
  baseline: any;
43
- };
49
+ }>;
50
+ /**
51
+ * Generate HTML report for TDD results
52
+ * @param {Object} results - TDD comparison results
53
+ */
54
+ generateHtmlReport(results: any): Promise<string>;
55
+ /**
56
+ * Open HTML report in default browser
57
+ * @param {string} reportPath - Path to HTML report
58
+ */
59
+ openReport(reportPath: string): Promise<void>;
44
60
  /**
45
61
  * Update baselines with current screenshots (accept changes)
46
62
  * @returns {number} Number of baselines updated
47
63
  */
48
64
  updateBaselines(): number;
65
+ /**
66
+ * Create a new baseline (used during --set-baseline mode)
67
+ * @private
68
+ */
69
+ private createNewBaseline;
49
70
  /**
50
71
  * Update a single baseline with current screenshot
51
72
  * @private
@@ -8,7 +8,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
8
8
  command: any;
9
9
  upload?: {};
10
10
  }, options?: {}): {
11
- upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, onProgress, }: {
11
+ upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, parallelId, onProgress, }: {
12
12
  screenshotsDir: any;
13
13
  buildName: any;
14
14
  branch: any;
@@ -17,6 +17,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
17
17
  environment?: string;
18
18
  threshold: any;
19
19
  pullRequestNumber: any;
20
+ parallelId: any;
20
21
  onProgress?: () => void;
21
22
  }) => Promise<{
22
23
  success: boolean;
@@ -16,6 +16,9 @@ export function loadConfig(configPath?: any, cliOverrides?: {}): Promise<{
16
16
  comparison: {
17
17
  threshold: number;
18
18
  };
19
+ tdd: {
20
+ openReport: boolean;
21
+ };
19
22
  apiKey: string;
20
23
  apiUrl: string;
21
24
  }>;
@@ -37,6 +37,11 @@ export function getServerUrl(): string | undefined;
37
37
  * @returns {string|undefined} Build ID
38
38
  */
39
39
  export function getBuildId(): string | undefined;
40
+ /**
41
+ * Get parallel ID from environment
42
+ * @returns {string|undefined} Parallel ID
43
+ */
44
+ export function getParallelId(): string | undefined;
40
45
  /**
41
46
  * Check if TDD mode is enabled
42
47
  * @returns {boolean} Whether TDD mode is enabled
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
3
+ * @param {string} name - Original screenshot name
4
+ * @param {number} maxLength - Maximum allowed length (default: 255)
5
+ * @returns {string} Sanitized screenshot name
6
+ */
7
+ export function sanitizeScreenshotName(name: string, maxLength?: number): string;
8
+ /**
9
+ * Validates that a path stays within the allowed working directory bounds
10
+ * @param {string} targetPath - Path to validate
11
+ * @param {string} workingDir - Working directory that serves as the security boundary
12
+ * @returns {string} Resolved and normalized path if valid
13
+ * @throws {Error} If path is invalid or outside bounds
14
+ */
15
+ export function validatePathSecurity(targetPath: string, workingDir: string): string;
16
+ /**
17
+ * Safely constructs a path within the working directory
18
+ * @param {string} workingDir - Base working directory
19
+ * @param {...string} pathSegments - Path segments to join
20
+ * @returns {string} Safely constructed path
21
+ * @throws {Error} If resulting path would be outside working directory
22
+ */
23
+ export function safePath(workingDir: string, ...pathSegments: string[]): string;
24
+ /**
25
+ * Validates screenshot properties object for safe values
26
+ * @param {Object} properties - Properties to validate
27
+ * @returns {Object} Validated properties object
28
+ */
29
+ export function validateScreenshotProperties(properties?: any): any;
@@ -1,6 +1,6 @@
1
1
  import { cosmiconfigSync } from 'cosmiconfig';
2
2
  import { resolve } from 'path';
3
- import { getApiToken, getApiUrl } from './environment-config.js';
3
+ import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
4
4
  const DEFAULT_CONFIG = {
5
5
  // API Configuration
6
6
  apiKey: getApiToken(),
@@ -25,6 +25,10 @@ const DEFAULT_CONFIG = {
25
25
  // Comparison Configuration
26
26
  comparison: {
27
27
  threshold: 0.1
28
+ },
29
+ // TDD Configuration
30
+ tdd: {
31
+ openReport: false // Whether to auto-open HTML report in browser
28
32
  }
29
33
  };
30
34
  export async function loadConfig(configPath = null, cliOverrides = {}) {
@@ -42,6 +46,9 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
42
46
  },
43
47
  comparison: {
44
48
  ...DEFAULT_CONFIG.comparison
49
+ },
50
+ tdd: {
51
+ ...DEFAULT_CONFIG.tdd
45
52
  }
46
53
  };
47
54
 
@@ -55,8 +62,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
55
62
  // 2. Override with environment variables
56
63
  const envApiKey = getApiToken();
57
64
  const envApiUrl = getApiUrl();
65
+ const envParallelId = getParallelId();
58
66
  if (envApiKey) config.apiKey = envApiKey;
59
67
  if (envApiUrl !== 'https://vizzly.dev') config.apiUrl = envApiUrl;
68
+ if (envParallelId) config.parallelId = envParallelId;
60
69
 
61
70
  // 3. Apply CLI overrides (highest priority)
62
71
  applyCLIOverrides(config, cliOverrides);
@@ -78,6 +87,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
78
87
  if (cliOverrides.branch) config.build.branch = cliOverrides.branch;
79
88
  if (cliOverrides.commit) config.build.commit = cliOverrides.commit;
80
89
  if (cliOverrides.message) config.build.message = cliOverrides.message;
90
+ if (cliOverrides.parallelId) config.parallelId = cliOverrides.parallelId;
81
91
 
82
92
  // Server overrides
83
93
  if (cliOverrides.port) config.server.port = parseInt(cliOverrides.port, 10);
@@ -59,6 +59,14 @@ export function getBuildId() {
59
59
  return process.env.VIZZLY_BUILD_ID;
60
60
  }
61
61
 
62
+ /**
63
+ * Get parallel ID from environment
64
+ * @returns {string|undefined} Parallel ID
65
+ */
66
+ export function getParallelId() {
67
+ return process.env.VIZZLY_PARALLEL_ID;
68
+ }
69
+
62
70
  /**
63
71
  * Check if TDD mode is enabled
64
72
  * @returns {boolean} Whether TDD mode is enabled
@@ -88,6 +96,7 @@ export function getAllEnvironmentConfig() {
88
96
  enabled: isVizzlyEnabled(),
89
97
  serverUrl: getServerUrl(),
90
98
  buildId: getBuildId(),
99
+ parallelId: getParallelId(),
91
100
  tddMode: isTddMode()
92
101
  };
93
102
  }