@vizzly-testing/cli 0.7.1 → 0.8.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.
- package/README.md +1 -1
- package/dist/commands/upload.js +3 -1
- package/dist/sdk/index.js +1 -1
- package/dist/services/api-service.js +41 -12
- package/dist/services/uploader.js +34 -16
- package/dist/types/sdk/index.d.ts +1 -1
- package/dist/types/services/api-service.d.ts +2 -2
- package/dist/utils/config-loader.js +1 -1
- package/dist/utils/console-ui.js +2 -1
- package/dist/utils/environment-config.js +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/doctor-command.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -368,7 +368,7 @@ Check if Vizzly is enabled in the current environment.
|
|
|
368
368
|
|
|
369
369
|
### Core Configuration
|
|
370
370
|
- `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
|
|
371
|
-
- `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
|
|
371
|
+
- `VIZZLY_API_URL`: Override API base URL. Default: `https://app.vizzly.dev`.
|
|
372
372
|
- `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
|
|
373
373
|
|
|
374
374
|
### Parallel Builds
|
package/dist/commands/upload.js
CHANGED
|
@@ -190,7 +190,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
190
190
|
// Silent fail on cleanup
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
-
|
|
193
|
+
// Use user-friendly error message if available
|
|
194
|
+
const errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
|
|
195
|
+
ui.error(errorMessage || 'Upload failed', error);
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
package/dist/sdk/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { VizzlyError } from '../errors/vizzly-error.js';
|
|
|
30
30
|
*
|
|
31
31
|
* const vizzly = await createVizzly({
|
|
32
32
|
* apiKey: process.env.VIZZLY_TOKEN,
|
|
33
|
-
* apiUrl: 'https://vizzly.dev',
|
|
33
|
+
* apiUrl: 'https://app.vizzly.dev',
|
|
34
34
|
* server: {
|
|
35
35
|
* port: 3003,
|
|
36
36
|
* enabled: true
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { URLSearchParams } from 'url';
|
|
7
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
7
|
+
import { VizzlyError, AuthError } from '../errors/vizzly-error.js';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import { getPackageVersion } from '../utils/package-info.js';
|
|
10
10
|
import { getApiUrl, getApiToken, getUserAgent } from '../utils/environment-config.js';
|
|
@@ -58,6 +58,11 @@ export class ApiService {
|
|
|
58
58
|
} catch {
|
|
59
59
|
// ignore
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// Handle authentication errors with user-friendly messages
|
|
63
|
+
if (response.status === 401) {
|
|
64
|
+
throw new AuthError('Invalid or expired API token. Please check your VIZZLY_TOKEN environment variable and ensure it is valid.');
|
|
65
|
+
}
|
|
61
66
|
throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (URL: ${url})`);
|
|
62
67
|
}
|
|
63
68
|
return response.json();
|
|
@@ -113,29 +118,44 @@ export class ApiService {
|
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
120
|
* Check if SHAs already exist on the server
|
|
116
|
-
* @param {string[]} shas - Array of SHA256 hashes to check
|
|
121
|
+
* @param {string[]|Object[]} shas - Array of SHA256 hashes to check, or array of screenshot objects with metadata
|
|
117
122
|
* @param {string} buildId - Build ID for screenshot record creation
|
|
118
123
|
* @returns {Promise<Object>} Response with existing SHAs and screenshot data
|
|
119
124
|
*/
|
|
120
125
|
async checkShas(shas, buildId) {
|
|
121
126
|
try {
|
|
127
|
+
let requestBody;
|
|
128
|
+
|
|
129
|
+
// Check if we're using the new signature-based format (array of objects) or legacy format (array of strings)
|
|
130
|
+
if (Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' && shas[0].sha256) {
|
|
131
|
+
// New signature-based format
|
|
132
|
+
requestBody = {
|
|
133
|
+
buildId,
|
|
134
|
+
screenshots: shas
|
|
135
|
+
};
|
|
136
|
+
} else {
|
|
137
|
+
// Legacy SHA-only format
|
|
138
|
+
requestBody = {
|
|
139
|
+
shas,
|
|
140
|
+
buildId
|
|
141
|
+
};
|
|
142
|
+
}
|
|
122
143
|
const response = await this.request('/api/sdk/check-shas', {
|
|
123
144
|
method: 'POST',
|
|
124
145
|
headers: {
|
|
125
146
|
'Content-Type': 'application/json'
|
|
126
147
|
},
|
|
127
|
-
body: JSON.stringify(
|
|
128
|
-
shas,
|
|
129
|
-
buildId
|
|
130
|
-
})
|
|
148
|
+
body: JSON.stringify(requestBody)
|
|
131
149
|
});
|
|
132
150
|
return response;
|
|
133
151
|
} catch (error) {
|
|
134
152
|
// Continue without deduplication on error
|
|
135
153
|
console.debug('SHA check failed, continuing without deduplication:', error.message);
|
|
154
|
+
// Extract SHAs for fallback response regardless of format
|
|
155
|
+
const shaList = Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' ? shas.map(s => s.sha256) : shas;
|
|
136
156
|
return {
|
|
137
157
|
existing: [],
|
|
138
|
-
missing:
|
|
158
|
+
missing: shaList,
|
|
139
159
|
screenshots: []
|
|
140
160
|
};
|
|
141
161
|
}
|
|
@@ -167,13 +187,22 @@ export class ApiService {
|
|
|
167
187
|
});
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
// Normal flow with SHA deduplication
|
|
190
|
+
// Normal flow with SHA deduplication using signature-based format
|
|
171
191
|
const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
172
192
|
|
|
173
|
-
//
|
|
174
|
-
const
|
|
193
|
+
// Create screenshot object with signature data for checking
|
|
194
|
+
const screenshotCheck = [{
|
|
195
|
+
sha256,
|
|
196
|
+
name,
|
|
197
|
+
browser: metadata?.browser || 'chrome',
|
|
198
|
+
viewport_width: metadata?.viewport?.width || 1920,
|
|
199
|
+
viewport_height: metadata?.viewport?.height || 1080
|
|
200
|
+
}];
|
|
201
|
+
|
|
202
|
+
// Check if this SHA with signature already exists
|
|
203
|
+
const checkResult = await this.checkShas(screenshotCheck, buildId);
|
|
175
204
|
if (checkResult.existing && checkResult.existing.includes(sha256)) {
|
|
176
|
-
// File already exists, screenshot record was automatically created
|
|
205
|
+
// File already exists with same signature, screenshot record was automatically created
|
|
177
206
|
const screenshot = checkResult.screenshots?.find(s => s.sha256 === sha256);
|
|
178
207
|
return {
|
|
179
208
|
message: 'Screenshot already exists, skipped upload',
|
|
@@ -184,7 +213,7 @@ export class ApiService {
|
|
|
184
213
|
};
|
|
185
214
|
}
|
|
186
215
|
|
|
187
|
-
// File doesn't exist, proceed with upload
|
|
216
|
+
// File doesn't exist or has different signature, proceed with upload
|
|
188
217
|
return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
189
218
|
method: 'POST',
|
|
190
219
|
headers: {
|
|
@@ -274,29 +274,31 @@ async function processFiles(files, signal, onProgress) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
/**
|
|
277
|
-
* Check which files already exist on the server
|
|
277
|
+
* Check which files already exist on the server using signature-based deduplication
|
|
278
278
|
*/
|
|
279
279
|
async function checkExistingFiles(fileMetadata, api, signal, buildId) {
|
|
280
|
-
const allShas = fileMetadata.map(f => f.sha256);
|
|
281
280
|
const existingShas = new Set();
|
|
282
281
|
const allScreenshots = [];
|
|
283
282
|
|
|
284
|
-
// Check in batches
|
|
285
|
-
for (let i = 0; i <
|
|
283
|
+
// Check in batches using the new signature-based format
|
|
284
|
+
for (let i = 0; i < fileMetadata.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
|
|
286
285
|
if (signal.aborted) throw new UploadError('Operation cancelled');
|
|
287
|
-
const batch =
|
|
286
|
+
const batch = fileMetadata.slice(i, i + DEFAULT_SHA_CHECK_BATCH_SIZE);
|
|
287
|
+
|
|
288
|
+
// Convert file metadata to screenshot objects with signature data
|
|
289
|
+
const screenshotBatch = batch.map(file => ({
|
|
290
|
+
sha256: file.sha256,
|
|
291
|
+
name: file.filename.replace(/\.png$/, ''),
|
|
292
|
+
// Remove .png extension for name
|
|
293
|
+
// Extract browser from filename if available (e.g., "homepage-chrome.png" -> "chrome")
|
|
294
|
+
browser: extractBrowserFromFilename(file.filename) || 'chrome',
|
|
295
|
+
// Default to chrome
|
|
296
|
+
// Default viewport dimensions (these could be extracted from filename or metadata if available)
|
|
297
|
+
viewport_width: 1920,
|
|
298
|
+
viewport_height: 1080
|
|
299
|
+
}));
|
|
288
300
|
try {
|
|
289
|
-
const res = await api.
|
|
290
|
-
method: 'POST',
|
|
291
|
-
headers: {
|
|
292
|
-
'Content-Type': 'application/json'
|
|
293
|
-
},
|
|
294
|
-
body: JSON.stringify({
|
|
295
|
-
shas: batch,
|
|
296
|
-
buildId
|
|
297
|
-
}),
|
|
298
|
-
signal
|
|
299
|
-
});
|
|
301
|
+
const res = await api.checkShas(screenshotBatch, buildId);
|
|
300
302
|
const {
|
|
301
303
|
existing = [],
|
|
302
304
|
screenshots = []
|
|
@@ -315,6 +317,22 @@ async function checkExistingFiles(fileMetadata, api, signal, buildId) {
|
|
|
315
317
|
};
|
|
316
318
|
}
|
|
317
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Extract browser name from filename
|
|
322
|
+
* @param {string} filename - The screenshot filename
|
|
323
|
+
* @returns {string|null} Browser name or null if not found
|
|
324
|
+
*/
|
|
325
|
+
function extractBrowserFromFilename(filename) {
|
|
326
|
+
const browsers = ['chrome', 'firefox', 'safari', 'edge', 'webkit'];
|
|
327
|
+
const lowerFilename = filename.toLowerCase();
|
|
328
|
+
for (const browser of browsers) {
|
|
329
|
+
if (lowerFilename.includes(browser)) {
|
|
330
|
+
return browser;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
318
336
|
/**
|
|
319
337
|
* Upload files to Vizzly
|
|
320
338
|
*/
|
|
@@ -41,11 +41,11 @@ export class ApiService {
|
|
|
41
41
|
createBuild(metadata: any): Promise<any>;
|
|
42
42
|
/**
|
|
43
43
|
* Check if SHAs already exist on the server
|
|
44
|
-
* @param {string[]} shas - Array of SHA256 hashes to check
|
|
44
|
+
* @param {string[]|Object[]} shas - Array of SHA256 hashes to check, or array of screenshot objects with metadata
|
|
45
45
|
* @param {string} buildId - Build ID for screenshot record creation
|
|
46
46
|
* @returns {Promise<Object>} Response with existing SHAs and screenshot data
|
|
47
47
|
*/
|
|
48
|
-
checkShas(shas: string[], buildId: string): Promise<any>;
|
|
48
|
+
checkShas(shas: string[] | any[], buildId: string): Promise<any>;
|
|
49
49
|
/**
|
|
50
50
|
* Upload a screenshot with SHA checking
|
|
51
51
|
* @param {string} buildId - Build ID
|
|
@@ -64,7 +64,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
64
64
|
const envApiUrl = getApiUrl();
|
|
65
65
|
const envParallelId = getParallelId();
|
|
66
66
|
if (envApiKey) config.apiKey = envApiKey;
|
|
67
|
-
if (envApiUrl !== 'https://vizzly.dev') config.apiUrl = envApiUrl;
|
|
67
|
+
if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
|
|
68
68
|
if (envParallelId) config.parallelId = envParallelId;
|
|
69
69
|
|
|
70
70
|
// 3. Apply CLI overrides (highest priority)
|
package/dist/utils/console-ui.js
CHANGED
|
@@ -43,9 +43,10 @@ export class ConsoleUI {
|
|
|
43
43
|
timestamp: new Date().toISOString()
|
|
44
44
|
};
|
|
45
45
|
if (error instanceof Error) {
|
|
46
|
+
const errorMessage = error.getUserMessage ? error.getUserMessage() : error.message;
|
|
46
47
|
errorData.error = {
|
|
47
48
|
name: error.name,
|
|
48
|
-
message:
|
|
49
|
+
message: errorMessage,
|
|
49
50
|
...(this.verbose && {
|
|
50
51
|
stack: error.stack
|
|
51
52
|
})
|
package/docs/api-reference.md
CHANGED
|
@@ -470,7 +470,7 @@ Configuration loaded via cosmiconfig in this order:
|
|
|
470
470
|
{
|
|
471
471
|
// API Configuration
|
|
472
472
|
apiKey: string, // API token (from VIZZLY_TOKEN)
|
|
473
|
-
apiUrl: string, // API base URL (default: 'https://vizzly.dev')
|
|
473
|
+
apiUrl: string, // API base URL (default: 'https://app.vizzly.dev')
|
|
474
474
|
project: string, // Project ID override
|
|
475
475
|
|
|
476
476
|
// Server Configuration (for run command)
|
package/docs/doctor-command.md
CHANGED
|
@@ -30,7 +30,7 @@ vizzly doctor --json
|
|
|
30
30
|
|
|
31
31
|
## Environment Variables
|
|
32
32
|
|
|
33
|
-
- `VIZZLY_API_URL` — Override the API base URL (default: `https://vizzly.dev`)
|
|
33
|
+
- `VIZZLY_API_URL` — Override the API base URL (default: `https://app.vizzly.dev`)
|
|
34
34
|
- `VIZZLY_TOKEN` — API token used only when `--api` is provided
|
|
35
35
|
|
|
36
36
|
## Exit Codes
|