@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.
@@ -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
+ }
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;
102
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) {
@@ -114,13 +133,12 @@ export class TestRunner extends BaseService {
114
133
  const apiService = await this.createApiService();
115
134
  if (apiService) {
116
135
  const buildResult = await apiService.createBuild({
117
- build: {
118
- name: options.buildName || `Test Run ${new Date().toISOString()}`,
119
- branch: options.branch || 'main',
120
- environment: options.environment || 'test',
121
- commit_sha: options.commit,
122
- commit_message: options.message
123
- }
136
+ name: options.buildName || `Test Run ${new Date().toISOString()}`,
137
+ branch: options.branch || 'main',
138
+ environment: options.environment || 'test',
139
+ commit_sha: options.commit,
140
+ commit_message: options.message,
141
+ github_pull_request_number: options.pullRequestNumber
124
142
  });
125
143
  this.logger.debug(`Build created with ID: ${buildResult.id}`);
126
144
 
@@ -154,10 +172,13 @@ export class TestRunner extends BaseService {
154
172
  }
155
173
  try {
156
174
  if (isTddMode) {
157
- // TDD mode: use server handler to finalize
175
+ // TDD mode: use server handler to finalize (local-only)
158
176
  if (this.serverManager.server?.finishBuild) {
159
177
  await this.serverManager.server.finishBuild(buildId);
160
178
  this.logger.debug(`TDD build ${buildId} finalized with success: ${success}`);
179
+ } else {
180
+ // In TDD mode without a server, just log that finalization is skipped
181
+ this.logger.debug(`TDD build ${buildId} finalization skipped (local-only mode)`);
161
182
  }
162
183
  } else {
163
184
  // API mode: use API service to update build status
@@ -188,8 +209,11 @@ export class TestRunner extends BaseService {
188
209
  this.testProcess.on('error', error => {
189
210
  reject(new VizzlyError(`Failed to run test command: ${error.message}`), 'TEST_COMMAND_FAILED');
190
211
  });
191
- this.testProcess.on('exit', code => {
192
- if (code !== 0) {
212
+ this.testProcess.on('exit', (code, signal) => {
213
+ // If process was killed by SIGINT, treat as interruption
214
+ if (signal === 'SIGINT') {
215
+ reject(new VizzlyError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
216
+ } else if (code !== 0) {
193
217
  reject(new VizzlyError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
194
218
  } else {
195
219
  resolve();
@@ -198,8 +222,13 @@ export class TestRunner extends BaseService {
198
222
  });
199
223
  }
200
224
  async cancel() {
201
- if (this.testProcess) {
202
- this.testProcess.kill('SIGTERM');
225
+ if (this.testProcess && !this.testProcess.killed) {
226
+ this.testProcess.kill('SIGKILL');
227
+ }
228
+
229
+ // Stop server manager if running
230
+ if (this.serverManager) {
231
+ await this.serverManager.stop();
203
232
  }
204
233
  }
205
234
  }
@@ -50,6 +50,7 @@ export function createUploader({
50
50
  message,
51
51
  environment = 'production',
52
52
  threshold,
53
+ pullRequestNumber,
53
54
  onProgress = () => {}
54
55
  }) {
55
56
  try {
@@ -96,10 +97,11 @@ export function createUploader({
96
97
  const buildInfo = {
97
98
  name: buildName || `Upload ${new Date().toISOString()}`,
98
99
  branch: branch || (await getDefaultBranch()) || 'main',
99
- commitSha: commit,
100
- commitMessage: message,
100
+ commit_sha: commit,
101
+ commit_message: message,
101
102
  environment,
102
- threshold
103
+ threshold,
104
+ github_pull_request_number: pullRequestNumber
103
105
  };
104
106
  const build = await api.createBuild(buildInfo);
105
107
  const buildId = build.id;
@@ -3,10 +3,11 @@
3
3
  * @param {string} testCommand - Test command to execute
4
4
  * @param {Object} options - Command options
5
5
  * @param {Object} globalOptions - Global CLI options
6
+ * @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
6
7
  */
7
8
  export function tddCommand(testCommand: string, options?: any, globalOptions?: any): Promise<{
8
- success: boolean;
9
- exitCode: number;
9
+ result: any;
10
+ cleanup: Function;
10
11
  }>;
11
12
  /**
12
13
  * Validate TDD options
@@ -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;
@@ -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, onProgress, }: {
11
+ upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, onProgress, }: {
12
12
  screenshotsDir: any;
13
13
  buildName: any;
14
14
  branch: any;
@@ -16,6 +16,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
16
16
  message: any;
17
17
  environment?: string;
18
18
  threshold: any;
19
+ pullRequestNumber: any;
19
20
  onProgress?: () => void;
20
21
  }) => Promise<{
21
22
  success: boolean;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CI Environment Detection
3
+ *
4
+ * Generic functions to extract git and PR information from any CI provider
5
+ */
6
+ /**
7
+ * Get the branch name from CI environment variables
8
+ * @returns {string|null} Branch name or null if not available
9
+ */
10
+ export function getBranch(): string | null;
11
+ /**
12
+ * Get the commit SHA from CI environment variables
13
+ * @returns {string|null} Commit SHA or null if not available
14
+ */
15
+ export function getCommit(): string | null;
16
+ /**
17
+ * Get the commit message from CI environment variables
18
+ * @returns {string|null} Commit message or null if not available
19
+ */
20
+ export function getCommitMessage(): string | null;
21
+ /**
22
+ * Get the pull request number from CI environment variables
23
+ * @returns {number|null} PR number or null if not available/not a PR
24
+ */
25
+ export function getPullRequestNumber(): number | null;
26
+ /**
27
+ * Get the PR head SHA from CI environment variables
28
+ * @returns {string|null} PR head SHA or null if not available
29
+ */
30
+ export function getPullRequestHeadSha(): string | null;
31
+ /**
32
+ * Get the PR base SHA from CI environment variables
33
+ * @returns {string|null} PR base SHA or null if not available
34
+ */
35
+ export function getPullRequestBaseSha(): string | null;
36
+ /**
37
+ * Get the PR head ref (branch) from CI environment variables
38
+ * @returns {string|null} PR head ref or null if not available
39
+ */
40
+ export function getPullRequestHeadRef(): string | null;
41
+ /**
42
+ * Get the PR base ref (target branch) from CI environment variables
43
+ * @returns {string|null} PR base ref or null if not available
44
+ */
45
+ export function getPullRequestBaseRef(): string | null;
46
+ /**
47
+ * Check if we're currently in a pull request context
48
+ * @returns {boolean} True if in a PR context
49
+ */
50
+ export function isPullRequest(): boolean;
51
+ /**
52
+ * Get the CI provider name
53
+ * @returns {string} CI provider name or 'unknown'
54
+ */
55
+ export function getCIProvider(): string;
@@ -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
  }>;
@@ -9,6 +9,13 @@ export function generateBuildName(): string;
9
9
  * @returns {Promise<string|null>} Commit message or null if not available
10
10
  */
11
11
  export function getCommitMessage(cwd?: string): Promise<string | null>;
12
+ /**
13
+ * Detect commit message with override and environment variable support
14
+ * @param {string} override - Commit message override from CLI
15
+ * @param {string} cwd - Working directory
16
+ * @returns {Promise<string|null>} Commit message or null if not available
17
+ */
18
+ export function detectCommitMessage(override?: string, cwd?: string): Promise<string | null>;
12
19
  /**
13
20
  * Check if the working directory is a git repository
14
21
  * @param {string} cwd - Working directory
@@ -42,3 +49,8 @@ export function detectCommit(override?: string, cwd?: string): Promise<string |
42
49
  * @returns {Promise<string>}
43
50
  */
44
51
  export function generateBuildNameWithGit(override?: string, cwd?: string): Promise<string>;
52
+ /**
53
+ * Detect pull request number from CI environment
54
+ * @returns {number|null} Pull request number or null if not in PR context
55
+ */
56
+ export function detectPullRequestNumber(): number | null;
@@ -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;