@vizzly-testing/cli 0.3.2 → 0.5.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 (60) hide show
  1. package/README.md +50 -28
  2. package/dist/cli.js +34 -30
  3. package/dist/client/index.js +1 -1
  4. package/dist/commands/run.js +38 -11
  5. package/dist/commands/tdd.js +30 -18
  6. package/dist/commands/upload.js +56 -5
  7. package/dist/server/handlers/api-handler.js +83 -0
  8. package/dist/server/handlers/tdd-handler.js +138 -0
  9. package/dist/server/http-server.js +132 -0
  10. package/dist/services/api-service.js +22 -2
  11. package/dist/services/server-manager.js +45 -29
  12. package/dist/services/test-runner.js +66 -69
  13. package/dist/services/uploader.js +11 -4
  14. package/dist/types/commands/run.d.ts +4 -1
  15. package/dist/types/commands/tdd.d.ts +5 -1
  16. package/dist/types/sdk/index.d.ts +6 -6
  17. package/dist/types/server/handlers/api-handler.d.ts +49 -0
  18. package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
  19. package/dist/types/server/http-server.d.ts +5 -0
  20. package/dist/types/services/api-service.d.ts +1 -0
  21. package/dist/types/services/server-manager.d.ts +148 -3
  22. package/dist/types/services/test-runner.d.ts +1 -0
  23. package/dist/types/services/uploader.d.ts +2 -1
  24. package/dist/types/utils/ci-env.d.ts +55 -0
  25. package/dist/types/utils/config-helpers.d.ts +1 -1
  26. package/dist/types/utils/console-ui.d.ts +1 -1
  27. package/dist/types/utils/git.d.ts +12 -0
  28. package/dist/utils/ci-env.js +293 -0
  29. package/dist/utils/console-ui.js +4 -14
  30. package/dist/utils/git.js +38 -0
  31. package/docs/api-reference.md +17 -5
  32. package/docs/getting-started.md +1 -1
  33. package/docs/tdd-mode.md +9 -9
  34. package/docs/test-integration.md +9 -17
  35. package/docs/upload-command.md +7 -0
  36. package/package.json +4 -5
  37. package/dist/screenshot-wrapper.js +0 -68
  38. package/dist/server/index.js +0 -522
  39. package/dist/services/service-utils.js +0 -171
  40. package/dist/types/index.js +0 -153
  41. package/dist/types/screenshot-wrapper.d.ts +0 -27
  42. package/dist/types/server/index.d.ts +0 -38
  43. package/dist/types/services/service-utils.d.ts +0 -45
  44. package/dist/types/types/index.d.ts +0 -373
  45. package/dist/types/utils/diagnostics.d.ts +0 -69
  46. package/dist/types/utils/error-messages.d.ts +0 -42
  47. package/dist/types/utils/framework-detector.d.ts +0 -5
  48. package/dist/types/utils/help.d.ts +0 -11
  49. package/dist/types/utils/image-comparison.d.ts +0 -42
  50. package/dist/types/utils/package.d.ts +0 -1
  51. package/dist/types/utils/project-detection.d.ts +0 -19
  52. package/dist/types/utils/ui-helpers.d.ts +0 -23
  53. package/dist/utils/diagnostics.js +0 -184
  54. package/dist/utils/error-messages.js +0 -34
  55. package/dist/utils/framework-detector.js +0 -40
  56. package/dist/utils/help.js +0 -66
  57. package/dist/utils/image-comparison.js +0 -172
  58. package/dist/utils/package.js +0 -9
  59. package/dist/utils/project-detection.js +0 -145
  60. package/dist/utils/ui-helpers.js +0 -86
@@ -0,0 +1,138 @@
1
+ import { Buffer } from 'buffer';
2
+ import { createServiceLogger } from '../../utils/logger-factory.js';
3
+ import { TddService } from '../../services/tdd-service.js';
4
+ import { colors } from '../../utils/colors.js';
5
+ const logger = createServiceLogger('TDD-HANDLER');
6
+ export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
7
+ const tddService = new TddService(config, workingDir);
8
+ const builds = new Map();
9
+ const initialize = async () => {
10
+ logger.info('🔄 TDD mode enabled - setting up local comparison...');
11
+ const baseline = await tddService.loadBaseline();
12
+ if (!baseline) {
13
+ if (config.apiKey) {
14
+ logger.info('📥 No local baseline found, downloading from Vizzly...');
15
+ await tddService.downloadBaselines(baselineBuild, baselineComparison);
16
+ } else {
17
+ logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
18
+ }
19
+ } else {
20
+ logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
21
+ }
22
+ };
23
+ const registerBuild = buildId => {
24
+ builds.set(buildId, {
25
+ id: buildId,
26
+ name: `TDD Build ${buildId}`,
27
+ branch: 'current',
28
+ environment: 'test',
29
+ screenshots: [],
30
+ createdAt: Date.now()
31
+ });
32
+ logger.debug(`Registered TDD build: ${buildId}`);
33
+ };
34
+ const handleScreenshot = async (buildId, name, image, properties = {}) => {
35
+ const build = builds.get(buildId);
36
+ if (!build) {
37
+ throw new Error(`Build ${buildId} not found`);
38
+ }
39
+ const screenshot = {
40
+ name,
41
+ imageData: image,
42
+ properties,
43
+ timestamp: Date.now()
44
+ };
45
+ build.screenshots.push(screenshot);
46
+ const imageBuffer = Buffer.from(image, 'base64');
47
+ const comparison = await tddService.compareScreenshot(name, imageBuffer, properties);
48
+ if (comparison.status === 'failed') {
49
+ return {
50
+ statusCode: 422,
51
+ body: {
52
+ error: 'Visual difference detected',
53
+ details: `Screenshot '${name}' differs from baseline`,
54
+ comparison: {
55
+ name: comparison.name,
56
+ status: comparison.status,
57
+ baseline: comparison.baseline,
58
+ current: comparison.current,
59
+ diff: comparison.diff
60
+ },
61
+ tddMode: true
62
+ }
63
+ };
64
+ }
65
+ if (comparison.status === 'baseline-updated') {
66
+ return {
67
+ statusCode: 200,
68
+ body: {
69
+ status: 'success',
70
+ message: `Baseline updated for ${name}`,
71
+ comparison: {
72
+ name: comparison.name,
73
+ status: comparison.status,
74
+ baseline: comparison.baseline,
75
+ current: comparison.current
76
+ },
77
+ tddMode: true
78
+ }
79
+ };
80
+ }
81
+ if (comparison.status === 'error') {
82
+ return {
83
+ statusCode: 500,
84
+ body: {
85
+ error: `Comparison failed: ${comparison.error}`,
86
+ tddMode: true
87
+ }
88
+ };
89
+ }
90
+ logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
91
+ return {
92
+ statusCode: 200,
93
+ body: {
94
+ success: true,
95
+ comparison: {
96
+ name: comparison.name,
97
+ status: comparison.status
98
+ },
99
+ tddMode: true
100
+ }
101
+ };
102
+ };
103
+ const getScreenshotCount = buildId => {
104
+ const build = builds.get(buildId);
105
+ return build ? build.screenshots.length : 0;
106
+ };
107
+ const finishBuild = async buildId => {
108
+ const build = builds.get(buildId);
109
+ if (!build) {
110
+ throw new Error(`Build ${buildId} not found`);
111
+ }
112
+ if (build.screenshots.length === 0) {
113
+ throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
114
+ }
115
+ const results = tddService.printResults();
116
+ builds.delete(buildId);
117
+ return {
118
+ id: buildId,
119
+ name: build.name,
120
+ tddMode: true,
121
+ results,
122
+ url: null,
123
+ passed: results.failed === 0 && results.errors === 0
124
+ };
125
+ };
126
+ const cleanup = () => {
127
+ builds.clear();
128
+ logger.debug('TDD handler cleanup completed');
129
+ };
130
+ return {
131
+ initialize,
132
+ registerBuild,
133
+ handleScreenshot,
134
+ getScreenshotCount,
135
+ finishBuild,
136
+ cleanup
137
+ };
138
+ };
@@ -0,0 +1,132 @@
1
+ import { createServer } from 'http';
2
+ import { createServiceLogger } from '../utils/logger-factory.js';
3
+ const logger = createServiceLogger('HTTP-SERVER');
4
+ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
5
+ let server = null;
6
+ const parseRequestBody = req => {
7
+ return new Promise((resolve, reject) => {
8
+ let body = '';
9
+ req.on('data', chunk => {
10
+ body += chunk.toString();
11
+ });
12
+ req.on('end', () => {
13
+ try {
14
+ const data = JSON.parse(body);
15
+ resolve(data);
16
+ } catch {
17
+ reject(new Error('Invalid JSON'));
18
+ }
19
+ });
20
+ req.on('error', reject);
21
+ });
22
+ };
23
+ const handleRequest = async (req, res) => {
24
+ res.setHeader('Access-Control-Allow-Origin', '*');
25
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
26
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
27
+ res.setHeader('Content-Type', 'application/json');
28
+ if (req.method === 'OPTIONS') {
29
+ res.statusCode = 200;
30
+ res.end();
31
+ return;
32
+ }
33
+ if (req.method === 'GET' && req.url === '/health') {
34
+ res.statusCode = 200;
35
+ res.end(JSON.stringify({
36
+ status: 'ok',
37
+ port: port,
38
+ uptime: process.uptime()
39
+ }));
40
+ return;
41
+ }
42
+ if (req.method === 'POST' && req.url === '/screenshot') {
43
+ try {
44
+ const body = await parseRequestBody(req);
45
+ const {
46
+ buildId,
47
+ name,
48
+ properties,
49
+ image
50
+ } = body;
51
+ if (!buildId || !name || !image) {
52
+ res.statusCode = 400;
53
+ res.end(JSON.stringify({
54
+ error: 'buildId, name, and image are required'
55
+ }));
56
+ return;
57
+ }
58
+ const result = await screenshotHandler.handleScreenshot(buildId, name, image, properties);
59
+
60
+ // Emit screenshot captured event if emitter is available
61
+ if (emitter && result.statusCode === 200) {
62
+ emitter.emit('screenshot-captured', {
63
+ name,
64
+ count: screenshotHandler.getScreenshotCount?.(buildId) || 0,
65
+ skipped: result.body?.skipped
66
+ });
67
+ }
68
+ res.statusCode = result.statusCode;
69
+ res.end(JSON.stringify(result.body));
70
+ } catch (error) {
71
+ logger.error('Screenshot processing error:', error);
72
+ res.statusCode = 500;
73
+ res.end(JSON.stringify({
74
+ error: 'Failed to process screenshot'
75
+ }));
76
+ }
77
+ return;
78
+ }
79
+ res.statusCode = 404;
80
+ res.end(JSON.stringify({
81
+ error: 'Not found'
82
+ }));
83
+ };
84
+ const start = () => {
85
+ return new Promise((resolve, reject) => {
86
+ server = createServer(async (req, res) => {
87
+ try {
88
+ await handleRequest(req, res);
89
+ } catch (error) {
90
+ logger.error('Server error:', error);
91
+ res.statusCode = 500;
92
+ res.setHeader('Content-Type', 'application/json');
93
+ res.end(JSON.stringify({
94
+ error: 'Internal server error'
95
+ }));
96
+ }
97
+ });
98
+ server.listen(port, '127.0.0.1', error => {
99
+ if (error) {
100
+ reject(error);
101
+ } else {
102
+ logger.debug(`HTTP server listening on http://127.0.0.1:${port}`);
103
+ resolve();
104
+ }
105
+ });
106
+ server.on('error', error => {
107
+ if (error.code === 'EADDRINUSE') {
108
+ reject(new Error(`Port ${port} is already in use. Try a different port with --port.`));
109
+ } else {
110
+ reject(error);
111
+ }
112
+ });
113
+ });
114
+ };
115
+ const stop = () => {
116
+ if (server) {
117
+ return new Promise(resolve => {
118
+ server.close(() => {
119
+ server = null;
120
+ logger.debug('HTTP server stopped');
121
+ resolve();
122
+ });
123
+ });
124
+ }
125
+ return Promise.resolve();
126
+ };
127
+ return {
128
+ start,
129
+ stop,
130
+ getServer: () => server
131
+ };
132
+ };
@@ -16,6 +16,7 @@ export class ApiService {
16
16
  constructor(options = {}) {
17
17
  this.baseUrl = options.baseUrl || getApiUrl();
18
18
  this.token = options.token || getApiToken();
19
+ this.uploadAll = options.uploadAll || false;
19
20
 
20
21
  // Build User-Agent string
21
22
  const command = options.command || 'run'; // Default to 'run' for API service
@@ -104,7 +105,9 @@ export class ApiService {
104
105
  headers: {
105
106
  'Content-Type': 'application/json'
106
107
  },
107
- body: JSON.stringify(metadata)
108
+ body: JSON.stringify({
109
+ build: metadata
110
+ })
108
111
  });
109
112
  }
110
113
 
@@ -147,7 +150,24 @@ export class ApiService {
147
150
  * @returns {Promise<Object>} Upload result
148
151
  */
149
152
  async uploadScreenshot(buildId, name, buffer, metadata = {}) {
150
- // Calculate SHA256 of the image
153
+ // Skip SHA deduplication entirely if uploadAll flag is set
154
+ if (this.uploadAll) {
155
+ // Upload directly without SHA calculation or checking
156
+ return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json'
160
+ },
161
+ body: JSON.stringify({
162
+ name,
163
+ image_data: buffer.toString('base64'),
164
+ properties: metadata ?? {}
165
+ // No SHA included when bypassing deduplication
166
+ })
167
+ });
168
+ }
169
+
170
+ // Normal flow with SHA deduplication
151
171
  const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
152
172
 
153
173
  // Check if this SHA already exists
@@ -1,44 +1,43 @@
1
1
  /**
2
2
  * Server Manager Service
3
- * Manages the Vizzly HTTP server
3
+ * Manages the HTTP server with functional handlers
4
4
  */
5
5
 
6
6
  import { BaseService } from './base-service.js';
7
- import { VizzlyServer } from '../server/index.js';
7
+ import { createHttpServer } from '../server/http-server.js';
8
+ import { createTddHandler } from '../server/handlers/tdd-handler.js';
9
+ import { createApiHandler } from '../server/handlers/api-handler.js';
8
10
  import { EventEmitter } from 'events';
9
11
  export class ServerManager extends BaseService {
10
12
  constructor(config, logger) {
11
13
  super(config, {
12
14
  logger
13
15
  });
14
- this.server = null;
16
+ this.httpServer = null;
17
+ this.handler = null;
18
+ this.emitter = null;
15
19
  }
16
- async start(buildId = null, buildInfo = null, mode = 'lazy') {
17
- if (this.started) {
18
- this.logger.warn(`${this.constructor.name} already started`);
19
- return;
20
- }
21
-
22
- // Create event emitter for server events
23
- const emitter = new EventEmitter();
24
- this.server = new VizzlyServer({
25
- port: this.config?.server?.port || 47392,
26
- config: this.config,
27
- buildId,
28
- buildInfo,
29
- vizzlyApi: buildInfo || mode === 'eager' ? await this.createApiService() : null,
30
- tddMode: mode === 'tdd',
31
- // TDD mode only when explicitly set
32
- baselineBuild: this.config?.baselineBuildId,
33
- baselineComparison: this.config?.baselineComparisonId,
34
- workingDir: process.cwd(),
35
- emitter // Pass the emitter to the server
36
- });
37
- await super.start();
20
+ async start(buildId = null, tddMode = false) {
21
+ this.buildId = buildId;
22
+ this.tddMode = tddMode;
23
+ return super.start();
38
24
  }
39
25
  async onStart() {
40
- if (this.server) {
41
- await this.server.start();
26
+ this.emitter = new EventEmitter();
27
+ const port = this.config?.server?.port || 47392;
28
+ if (this.tddMode) {
29
+ this.handler = createTddHandler(this.config, process.cwd(), this.config?.baselineBuildId, this.config?.baselineComparisonId);
30
+ await this.handler.initialize();
31
+ if (this.buildId) {
32
+ this.handler.registerBuild(this.buildId);
33
+ }
34
+ } else {
35
+ const apiService = await this.createApiService();
36
+ this.handler = createApiHandler(apiService);
37
+ }
38
+ this.httpServer = createHttpServer(port, this.handler, this.emitter);
39
+ if (this.httpServer) {
40
+ await this.httpServer.start();
42
41
  }
43
42
  }
44
43
  async createApiService() {
@@ -54,8 +53,25 @@ export class ServerManager extends BaseService {
54
53
  });
55
54
  }
56
55
  async onStop() {
57
- if (this.server) {
58
- await this.server.stop();
56
+ if (this.httpServer) {
57
+ await this.httpServer.stop();
59
58
  }
59
+ if (this.handler?.cleanup) {
60
+ try {
61
+ this.handler.cleanup();
62
+ } catch (error) {
63
+ this.logger.debug('Handler cleanup error:', error.message);
64
+ // Don't throw - cleanup errors shouldn't fail the stop process
65
+ }
66
+ }
67
+ }
68
+
69
+ // Expose server interface for compatibility
70
+ get server() {
71
+ return {
72
+ emitter: this.emitter,
73
+ getScreenshotCount: buildId => this.handler?.getScreenshotCount?.(buildId) || 0,
74
+ finishBuild: buildId => this.handler?.finishBuild?.(buildId)
75
+ };
60
76
  }
61
77
  }
@@ -40,87 +40,47 @@ export class TestRunner extends BaseService {
40
40
  };
41
41
  }
42
42
  try {
43
- let buildInfo = null;
44
43
  let buildUrl = null;
45
44
  let screenshotCount = 0;
46
- if (tdd) {
47
- // TDD mode: create local build for fast feedback
48
- this.logger.debug('TDD mode: creating local build...');
49
- const build = await this.buildManager.createBuild(options);
50
- buildId = build.id;
51
- this.logger.debug(`TDD build created with ID: ${build.id}`);
52
- } else if (options.eager) {
53
- // Eager mode: create build immediately via API
54
- this.logger.debug('Eager mode: creating build via API...');
45
+
46
+ // Create build based on mode
47
+ buildId = await this.createBuild(options, tdd);
48
+ if (!tdd && buildId) {
49
+ // Get build URL for API mode
55
50
  const apiService = await this.createApiService();
56
51
  if (apiService) {
57
- const buildResult = await apiService.createBuild({
58
- build: {
59
- name: options.buildName || `Test Run ${new Date().toISOString()}`,
60
- branch: options.branch || 'main',
61
- environment: options.environment || 'test',
62
- commit_sha: options.commit,
63
- commit_message: options.message
52
+ try {
53
+ const build = await apiService.getBuild(buildId);
54
+ buildUrl = build.url;
55
+ if (buildUrl) {
56
+ this.logger.info(`Build URL: ${buildUrl}`);
64
57
  }
65
- });
66
- buildId = buildResult.id;
67
- buildUrl = buildResult.url;
68
- this.logger.debug(`Eager build created with ID: ${buildId}`);
69
- if (buildUrl) {
70
- this.logger.info(`Build URL: ${buildUrl}`);
58
+ } catch (error) {
59
+ this.logger.debug('Could not retrieve build URL:', error.message);
71
60
  }
72
-
73
- // Emit build created event for eager mode
74
- this.emit('build-created', {
75
- buildId: buildResult.id,
76
- url: buildResult.url,
77
- name: buildResult.name || options.buildName
78
- });
79
- } else {
80
- this.logger.warn('No API key available for eager build creation, falling back to lazy mode');
81
61
  }
82
- } else {
83
- // Lazy mode: prepare build info for API creation on first screenshot
84
- buildInfo = {
85
- buildName: options.buildName || `Test Run ${new Date().toISOString()}`,
86
- branch: options.branch || 'main',
87
- environment: options.environment || 'test',
88
- commitSha: options.commit,
89
- commitMessage: options.message
90
- };
91
62
  }
92
63
 
93
- // Start server with appropriate configuration
94
- const mode = tdd ? 'tdd' : options.eager ? 'eager' : 'lazy';
95
- await this.serverManager.start(buildId, buildInfo, mode);
64
+ // Start server with appropriate handler
65
+ await this.serverManager.start(buildId, tdd);
96
66
 
97
67
  // Forward server events
98
- if (this.serverManager.server && this.serverManager.server.emitter) {
99
- this.serverManager.server.emitter.on('build-created', buildInfo => {
100
- // Update local buildId and buildUrl from server
101
- buildId = buildInfo.buildId;
102
- buildUrl = buildInfo.url;
103
- this.emit('build-created', buildInfo);
104
- });
68
+ if (this.serverManager.server?.emitter) {
105
69
  this.serverManager.server.emitter.on('screenshot-captured', screenshotInfo => {
106
70
  screenshotCount++;
107
71
  this.emit('screenshot-captured', screenshotInfo);
108
72
  });
109
73
  }
110
- if (tdd) {
111
- this.logger.debug('TDD service ready for comparisons');
112
- }
113
74
  const env = {
114
75
  ...process.env,
115
76
  VIZZLY_SERVER_URL: `http://localhost:${this.config.server.port}`,
116
- VIZZLY_BUILD_ID: buildId || 'lazy',
117
- // Use 'lazy' for API-driven builds
77
+ VIZZLY_BUILD_ID: buildId,
118
78
  VIZZLY_ENABLED: 'true',
119
79
  VIZZLY_SET_BASELINE: options.setBaseline || options['set-baseline'] ? 'true' : 'false'
120
80
  };
121
81
  await this.executeTestCommand(testCommand, env);
122
82
 
123
- // Finalize builds based on mode
83
+ // Finalize build
124
84
  const executionTime = Date.now() - startTime;
125
85
  await this.finalizeBuild(buildId, tdd, true, executionTime);
126
86
  return {
@@ -133,14 +93,45 @@ export class TestRunner extends BaseService {
133
93
  } catch (error) {
134
94
  this.logger.error('Test run failed:', error);
135
95
 
136
- // Finalize builds on failure too
96
+ // Finalize build on failure
137
97
  const executionTime = Date.now() - startTime;
138
98
  await this.finalizeBuild(buildId, tdd, false, executionTime);
139
99
  throw error;
140
100
  } finally {
141
101
  await this.serverManager.stop();
142
- if (tdd && this.tddService && typeof this.tddService.stop === 'function') {
143
- await this.tddService.stop();
102
+ }
103
+ }
104
+ async createBuild(options, tdd) {
105
+ if (tdd) {
106
+ // TDD mode: create local build
107
+ this.logger.debug('TDD mode: creating local build...');
108
+ const build = await this.buildManager.createBuild(options);
109
+ this.logger.debug(`TDD build created with ID: ${build.id}`);
110
+ return build.id;
111
+ } else {
112
+ // API mode: create build via API
113
+ this.logger.debug('Creating build via API...');
114
+ const apiService = await this.createApiService();
115
+ if (apiService) {
116
+ const buildResult = await apiService.createBuild({
117
+ name: options.buildName || `Test Run ${new Date().toISOString()}`,
118
+ branch: options.branch || 'main',
119
+ environment: options.environment || 'test',
120
+ commit_sha: options.commit,
121
+ commit_message: options.message,
122
+ github_pull_request_number: options.pullRequestNumber
123
+ });
124
+ this.logger.debug(`Build created with ID: ${buildResult.id}`);
125
+
126
+ // Emit build created event
127
+ this.emit('build-created', {
128
+ buildId: buildResult.id,
129
+ url: buildResult.url,
130
+ name: buildResult.name || options.buildName
131
+ });
132
+ return buildResult.id;
133
+ } else {
134
+ throw new VizzlyError('No API key available for build creation', 'API_KEY_MISSING');
144
135
  }
145
136
  }
146
137
  }
@@ -151,9 +142,8 @@ export class TestRunner extends BaseService {
151
142
  } = await import('./api-service.js');
152
143
  return new ApiService({
153
144
  ...this.config,
154
- command: 'run'
155
- }, {
156
- logger: this.logger
145
+ command: 'run',
146
+ uploadAll: this.config.uploadAll
157
147
  });
158
148
  }
159
149
  async finalizeBuild(buildId, isTddMode, success, executionTime) {
@@ -163,15 +153,16 @@ export class TestRunner extends BaseService {
163
153
  }
164
154
  try {
165
155
  if (isTddMode) {
166
- // TDD mode: use buildManager for local builds
167
- if (this.buildManager.getCurrentBuild()) {
168
- await this.buildManager.finalizeBuild(buildId, {
169
- success
170
- });
156
+ // TDD mode: use server handler to finalize (local-only)
157
+ if (this.serverManager.server?.finishBuild) {
158
+ await this.serverManager.server.finishBuild(buildId);
171
159
  this.logger.debug(`TDD build ${buildId} finalized with success: ${success}`);
160
+ } else {
161
+ // In TDD mode without a server, just log that finalization is skipped
162
+ this.logger.debug(`TDD build ${buildId} finalization skipped (local-only mode)`);
172
163
  }
173
164
  } else {
174
- // API mode (eager/lazy): use API service to update build status
165
+ // API mode: use API service to update build status
175
166
  const apiService = await this.createApiService();
176
167
  if (apiService) {
177
168
  await apiService.finalizeBuild(buildId, success, executionTime);
@@ -180,6 +171,12 @@ export class TestRunner extends BaseService {
180
171
  } catch (error) {
181
172
  // Don't fail the entire run if build finalization fails
182
173
  this.logger.warn(`Failed to finalize build ${buildId}:`, error.message);
174
+ // Emit event for UI handling
175
+ this.emit('build-finalize-failed', {
176
+ buildId,
177
+ error: error.message,
178
+ stack: error.stack
179
+ });
183
180
  }
184
181
  }
185
182
  async executeTestCommand(testCommand, env) {
@@ -11,7 +11,7 @@ import { createUploaderLogger } from '../utils/logger-factory.js';
11
11
  import { ApiService } from './api-service.js';
12
12
  import { getDefaultBranch } from '../utils/git.js';
13
13
  import { UploadError, TimeoutError, ValidationError } from '../errors/vizzly-error.js';
14
- const DEFAULT_BATCH_SIZE = 10;
14
+ const DEFAULT_BATCH_SIZE = 50;
15
15
  const DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
16
16
  const DEFAULT_TIMEOUT = 30000; // 30 seconds
17
17
 
@@ -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 {
@@ -80,12 +81,14 @@ export function createUploader({
80
81
  }
81
82
  onProgress({
82
83
  phase: 'scanning',
84
+ message: `Found ${files.length} screenshots`,
83
85
  total: files.length
84
86
  });
85
87
 
86
88
  // Process files to get metadata
87
89
  const fileMetadata = await processFiles(files, signal, current => onProgress({
88
90
  phase: 'processing',
91
+ message: `Processing files`,
89
92
  current,
90
93
  total: files.length
91
94
  }));
@@ -94,10 +97,11 @@ export function createUploader({
94
97
  const buildInfo = {
95
98
  name: buildName || `Upload ${new Date().toISOString()}`,
96
99
  branch: branch || (await getDefaultBranch()) || 'main',
97
- commitSha: commit,
98
- commitMessage: message,
100
+ commit_sha: commit,
101
+ commit_message: message,
99
102
  environment,
100
- threshold
103
+ threshold,
104
+ github_pull_request_number: pullRequestNumber
101
105
  };
102
106
  const build = await api.createBuild(buildInfo);
103
107
  const buildId = build.id;
@@ -110,6 +114,7 @@ export function createUploader({
110
114
  } = await checkExistingFiles(fileMetadata, api, signal, buildId);
111
115
  onProgress({
112
116
  phase: 'deduplication',
117
+ message: `Checking for duplicates (${toUpload.length} to upload, ${existing.length} existing)`,
113
118
  toUpload: toUpload.length,
114
119
  existing: existing.length,
115
120
  total: files.length
@@ -127,12 +132,14 @@ export function createUploader({
127
132
  batchSize: batchSize,
128
133
  onProgress: current => onProgress({
129
134
  phase: 'uploading',
135
+ message: `Uploading screenshots`,
130
136
  current,
131
137
  total: toUpload.length
132
138
  })
133
139
  });
134
140
  onProgress({
135
141
  phase: 'completed',
142
+ message: `Upload completed`,
136
143
  buildId: result.buildId,
137
144
  url: result.url
138
145
  });