@vizzly-testing/cli 0.2.0 → 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.
- package/dist/cli.js +1 -0
- package/dist/index.js +2 -0
- package/dist/services/api-service.js +18 -9
- package/dist/services/uploader.js +41 -81
- package/dist/types/services/api-service.d.ts +3 -2
- package/package.json +2 -1
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
|
149
|
-
if (
|
|
150
|
-
// File already exists,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
118
|
+
// Upload remaining files
|
|
106
119
|
const result = await uploadFiles({
|
|
107
120
|
toUpload,
|
|
108
121
|
existing,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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,
|
|
323
|
+
// If all files exist, screenshot records were already created during SHA check
|
|
313
324
|
if (toUpload.length === 0) {
|
|
314
|
-
return
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
|
372
|
-
url: result
|
|
362
|
+
buildId,
|
|
363
|
+
url: result?.build?.url || result?.url
|
|
373
364
|
};
|
|
374
365
|
}
|
|
375
366
|
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
* @
|
|
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<
|
|
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.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Visual review platform for UI developers and designers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visual-testing",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"commander": "^11.1.0",
|
|
77
77
|
"cosmiconfig": "^9.0.0",
|
|
78
|
+
"dotenv": "^17.2.1",
|
|
78
79
|
"form-data": "^4.0.0",
|
|
79
80
|
"glob": "^10.3.10",
|
|
80
81
|
"odiff-bin": "^3.2.1"
|