@vizzly-testing/cli 0.3.1 → 0.3.2

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.
@@ -111,9 +111,10 @@ export class ApiService {
111
111
  /**
112
112
  * Check if SHAs already exist on the server
113
113
  * @param {string[]} shas - Array of SHA256 hashes to check
114
- * @returns {Promise<string[]>} Array of existing SHAs
114
+ * @param {string} buildId - Build ID for screenshot record creation
115
+ * @returns {Promise<Object>} Response with existing SHAs and screenshot data
115
116
  */
116
- async checkShas(shas) {
117
+ async checkShas(shas, buildId) {
117
118
  try {
118
119
  const response = await this.request('/api/sdk/check-shas', {
119
120
  method: 'POST',
@@ -121,14 +122,19 @@ export class ApiService {
121
122
  'Content-Type': 'application/json'
122
123
  },
123
124
  body: JSON.stringify({
124
- shas
125
+ shas,
126
+ buildId
125
127
  })
126
128
  });
127
- return response.existing || [];
129
+ return response;
128
130
  } catch (error) {
129
131
  // Continue without deduplication on error
130
132
  console.debug('SHA check failed, continuing without deduplication:', error.message);
131
- return [];
133
+ return {
134
+ existing: [],
135
+ missing: shas,
136
+ screenshots: []
137
+ };
132
138
  }
133
139
  }
134
140
 
@@ -145,13 +151,16 @@ export class ApiService {
145
151
  const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
146
152
 
147
153
  // Check if this SHA already exists
148
- const existingShas = await this.checkShas([sha256]);
149
- if (existingShas.includes(sha256)) {
150
- // File already exists, skip upload but still register the screenshot
154
+ const checkResult = await this.checkShas([sha256], buildId);
155
+ if (checkResult.existing && checkResult.existing.includes(sha256)) {
156
+ // File already exists, screenshot record was automatically created
157
+ const screenshot = checkResult.screenshots?.find(s => s.sha256 === sha256);
151
158
  return {
152
159
  message: 'Screenshot already exists, skipped upload',
153
160
  sha256,
154
- skipped: true
161
+ skipped: true,
162
+ screenshot,
163
+ fromExisting: true
155
164
  };
156
165
  }
157
166
 
@@ -90,11 +90,24 @@ export function createUploader({
90
90
  total: files.length
91
91
  }));
92
92
 
93
- // Check which files need uploading
93
+ // Create build first to get buildId for SHA checking
94
+ const buildInfo = {
95
+ name: buildName || `Upload ${new Date().toISOString()}`,
96
+ branch: branch || (await getDefaultBranch()) || 'main',
97
+ commitSha: commit,
98
+ commitMessage: message,
99
+ environment,
100
+ threshold
101
+ };
102
+ const build = await api.createBuild(buildInfo);
103
+ const buildId = build.id;
104
+
105
+ // Check which files need uploading (now with buildId)
94
106
  const {
95
107
  toUpload,
96
- existing
97
- } = await checkExistingFiles(fileMetadata, api, signal);
108
+ existing,
109
+ screenshots
110
+ } = await checkExistingFiles(fileMetadata, api, signal, buildId);
98
111
  onProgress({
99
112
  phase: 'deduplication',
100
113
  toUpload: toUpload.length,
@@ -102,18 +115,13 @@ export function createUploader({
102
115
  total: files.length
103
116
  });
104
117
 
105
- // Create build and upload files
118
+ // Upload remaining files
106
119
  const result = await uploadFiles({
107
120
  toUpload,
108
121
  existing,
109
- buildInfo: {
110
- name: buildName || `Upload ${new Date().toISOString()}`,
111
- branch: branch || (await getDefaultBranch()) || 'main',
112
- commitSha: commit,
113
- commitMessage: message,
114
- environment,
115
- threshold
116
- },
122
+ screenshots,
123
+ buildId,
124
+ buildInfo,
117
125
  api,
118
126
  signal,
119
127
  batchSize: batchSize,
@@ -260,9 +268,10 @@ async function processFiles(files, signal, onProgress) {
260
268
  /**
261
269
  * Check which files already exist on the server
262
270
  */
263
- async function checkExistingFiles(fileMetadata, api, signal) {
271
+ async function checkExistingFiles(fileMetadata, api, signal, buildId) {
264
272
  const allShas = fileMetadata.map(f => f.sha256);
265
273
  const existingShas = new Set();
274
+ const allScreenshots = [];
266
275
 
267
276
  // Check in batches
268
277
  for (let i = 0; i < allShas.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
@@ -275,14 +284,17 @@ async function checkExistingFiles(fileMetadata, api, signal) {
275
284
  'Content-Type': 'application/json'
276
285
  },
277
286
  body: JSON.stringify({
278
- shas: batch
287
+ shas: batch,
288
+ buildId
279
289
  }),
280
290
  signal
281
291
  });
282
292
  const {
283
- existing = []
293
+ existing = [],
294
+ screenshots = []
284
295
  } = res || {};
285
296
  existing.forEach(sha => existingShas.add(sha));
297
+ allScreenshots.push(...screenshots);
286
298
  } catch (error) {
287
299
  // Continue without deduplication on error
288
300
  console.debug('SHA check failed, continuing without deduplication:', error.message);
@@ -290,7 +302,8 @@ async function checkExistingFiles(fileMetadata, api, signal) {
290
302
  }
291
303
  return {
292
304
  toUpload: fileMetadata.filter(f => !existingShas.has(f.sha256)),
293
- existing: fileMetadata.filter(f => existingShas.has(f.sha256))
305
+ existing: fileMetadata.filter(f => existingShas.has(f.sha256)),
306
+ screenshots: allScreenshots
294
307
  };
295
308
  }
296
309
 
@@ -299,49 +312,30 @@ async function checkExistingFiles(fileMetadata, api, signal) {
299
312
  */
300
313
  async function uploadFiles({
301
314
  toUpload,
302
- existing,
303
- buildInfo,
315
+ buildId,
304
316
  api,
305
317
  signal,
306
318
  batchSize,
307
319
  onProgress
308
320
  }) {
309
- let buildId = null;
310
321
  let result = null;
311
322
 
312
- // If all files exist, just create a build
323
+ // If all files exist, screenshot records were already created during SHA check
313
324
  if (toUpload.length === 0) {
314
- return createBuildWithExisting({
315
- existing,
316
- buildInfo,
317
- api,
318
- signal
319
- });
325
+ return {
326
+ buildId,
327
+ url: null
328
+ }; // Build was already created
320
329
  }
321
330
 
322
331
  // Upload in batches
323
332
  for (let i = 0; i < toUpload.length; i += batchSize) {
324
333
  if (signal.aborted) throw new UploadError('Operation cancelled');
325
334
  const batch = toUpload.slice(i, i + batchSize);
326
- const isFirstBatch = i === 0;
327
335
  const form = new FormData();
328
- if (isFirstBatch) {
329
- // First batch creates the build
330
- form.append('build_name', buildInfo.name);
331
- form.append('branch', buildInfo.branch);
332
- form.append('environment', buildInfo.environment);
333
- if (buildInfo.commitSha) form.append('commit_sha', buildInfo.commitSha);
334
- if (buildInfo.commitMessage) form.append('commit_message', buildInfo.commitMessage);
335
- if (buildInfo.threshold !== undefined) form.append('threshold', buildInfo.threshold.toString());
336
336
 
337
- // Include existing SHAs
338
- if (existing.length > 0) {
339
- form.append('existing_shas', JSON.stringify(existing.map(f => f.sha256)));
340
- }
341
- } else {
342
- // Subsequent batches add to existing build
343
- form.append('build_id', buildId);
344
- }
337
+ // All batches add to existing build (build was created earlier)
338
+ form.append('build_id', buildId);
345
339
 
346
340
  // Add files
347
341
  for (const file of batch) {
@@ -362,50 +356,16 @@ async function uploadFiles({
362
356
  batch: i / batchSize + 1
363
357
  });
364
358
  }
365
- if (isFirstBatch && result.build?.id) {
366
- buildId = result.build.id;
367
- }
368
359
  onProgress(i + batch.length);
369
360
  }
370
361
  return {
371
- buildId: result.build?.id || buildId,
372
- url: result.build?.url || result.url
362
+ buildId,
363
+ url: result?.build?.url || result?.url
373
364
  };
374
365
  }
375
366
 
376
- /**
377
- * Create a build with only existing files
378
- */
379
- async function createBuildWithExisting({
380
- existing,
381
- buildInfo,
382
- api,
383
- signal
384
- }) {
385
- const form = new FormData();
386
- form.append('build_name', buildInfo.name);
387
- form.append('branch', buildInfo.branch);
388
- form.append('environment', buildInfo.environment);
389
- form.append('existing_shas', JSON.stringify(existing.map(f => f.sha256)));
390
- if (buildInfo.commitSha) form.append('commit_sha', buildInfo.commitSha);
391
- if (buildInfo.commitMessage) form.append('commit_message', buildInfo.commitMessage);
392
- if (buildInfo.threshold !== undefined) form.append('threshold', buildInfo.threshold.toString());
393
- let result;
394
- try {
395
- result = await api.request('/api/sdk/upload', {
396
- method: 'POST',
397
- body: form,
398
- signal,
399
- headers: {}
400
- });
401
- } catch (err) {
402
- throw new UploadError(`Failed to create build: ${err.message}`);
403
- }
404
- return {
405
- buildId: result.build?.id,
406
- url: result.build?.url || result.url
407
- };
408
- }
367
+ // createBuildWithExisting function removed - no longer needed since
368
+ // builds are created first and /check-shas automatically creates screenshot records
409
369
 
410
370
  /**
411
371
  * Uploader class for handling screenshot uploads
@@ -41,9 +41,10 @@ export class ApiService {
41
41
  /**
42
42
  * Check if SHAs already exist on the server
43
43
  * @param {string[]} shas - Array of SHA256 hashes to check
44
- * @returns {Promise<string[]>} Array of existing SHAs
44
+ * @param {string} buildId - Build ID for screenshot record creation
45
+ * @returns {Promise<Object>} Response with existing SHAs and screenshot data
45
46
  */
46
- checkShas(shas: string[]): Promise<string[]>;
47
+ checkShas(shas: string[], buildId: string): Promise<any>;
47
48
  /**
48
49
  * Upload a screenshot with SHA checking
49
50
  * @param {string} buildId - Build ID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",