@vizzly-testing/cli 0.30.0 → 0.32.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 +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/region-coverage.js +109 -21
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/comparison-service.js +10 -1
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +144 -138
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/dist/commands/tdd.js
CHANGED
|
@@ -15,6 +15,9 @@ import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTest
|
|
|
15
15
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
16
16
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
|
|
17
17
|
import * as defaultOutput from '../utils/output.js';
|
|
18
|
+
function buildLocalContextCommand() {
|
|
19
|
+
return 'vizzly context build current --source local --agent';
|
|
20
|
+
}
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* TDD command implementation
|
|
@@ -91,7 +94,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
91
94
|
output.header('tdd', mode);
|
|
92
95
|
|
|
93
96
|
// Show config in verbose mode
|
|
94
|
-
output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold}`);
|
|
97
|
+
output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold} minClusterSize=${config.comparison.minClusterSize}`);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
// Create functional dependencies
|
|
@@ -133,6 +136,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
133
136
|
commit,
|
|
134
137
|
environment: config.build.environment,
|
|
135
138
|
threshold: config.comparison.threshold,
|
|
139
|
+
minClusterSize: config.comparison.minClusterSize,
|
|
136
140
|
allowNoToken: config.allowNoToken || false,
|
|
137
141
|
baselineBuildId: config.baselineBuildId,
|
|
138
142
|
baselineComparisonId: config.baselineComparisonId,
|
|
@@ -231,10 +235,15 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
231
235
|
exitCode,
|
|
232
236
|
comparisons,
|
|
233
237
|
summary,
|
|
234
|
-
reportPath: runResult.reportPath || '.vizzly/report/index.html'
|
|
238
|
+
reportPath: runResult.reportPath || '.vizzly/report/index.html',
|
|
239
|
+
contextCommand: buildLocalContextCommand()
|
|
235
240
|
});
|
|
236
241
|
output.cleanup();
|
|
237
242
|
}
|
|
243
|
+
if (!globalOptions.json && !options.daemon) {
|
|
244
|
+
output.blank?.();
|
|
245
|
+
output.print(` Context ${buildLocalContextCommand()}`);
|
|
246
|
+
}
|
|
238
247
|
return {
|
|
239
248
|
result: {
|
|
240
249
|
success: !hasFailures,
|
|
@@ -287,22 +296,28 @@ export function validateTddOptions(testCommand, options) {
|
|
|
287
296
|
errors.push('Test command is required');
|
|
288
297
|
}
|
|
289
298
|
if (options.port) {
|
|
290
|
-
let port =
|
|
291
|
-
if (Number.
|
|
299
|
+
let port = Number(options.port);
|
|
300
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
292
301
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
if (options.timeout) {
|
|
296
|
-
let timeout =
|
|
297
|
-
if (Number.
|
|
305
|
+
let timeout = Number(options.timeout);
|
|
306
|
+
if (!Number.isInteger(timeout) || timeout < 1000) {
|
|
298
307
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
299
308
|
}
|
|
300
309
|
}
|
|
301
310
|
if (options.threshold !== undefined) {
|
|
302
|
-
let threshold =
|
|
303
|
-
if (Number.
|
|
311
|
+
let threshold = Number(options.threshold);
|
|
312
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
304
313
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
305
314
|
}
|
|
306
315
|
}
|
|
316
|
+
if (options.minClusterSize !== undefined) {
|
|
317
|
+
let minClusterSize = Number(options.minClusterSize);
|
|
318
|
+
if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
|
|
319
|
+
errors.push('Min cluster size must be a positive integer');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
307
322
|
return errors;
|
|
308
323
|
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
2
|
-
import { createUploader as defaultCreateUploader } from '../
|
|
2
|
+
import { createUploader as defaultCreateUploader } from '../uploader/index.js';
|
|
3
|
+
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
3
4
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
4
5
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
5
6
|
import * as defaultOutput from '../utils/output.js';
|
|
@@ -26,7 +27,7 @@ export async function constructBuildUrl(buildId, apiUrl, apiToken, deps = {}) {
|
|
|
26
27
|
command: 'upload'
|
|
27
28
|
});
|
|
28
29
|
let tokenContext = await getTokenContext(client);
|
|
29
|
-
let baseUrl = apiUrl
|
|
30
|
+
let baseUrl = getAppBaseUrl(apiUrl);
|
|
30
31
|
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
31
32
|
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
|
|
32
33
|
}
|
|
@@ -38,7 +39,7 @@ export async function constructBuildUrl(buildId, apiUrl, apiToken, deps = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// Fallback URL construction
|
|
41
|
-
let baseUrl = apiUrl
|
|
42
|
+
let baseUrl = getAppBaseUrl(apiUrl);
|
|
42
43
|
return `${baseUrl}/builds/${buildId}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -375,20 +376,20 @@ export function validateUploadOptions(screenshotsPath, options) {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
if (options.threshold !== undefined) {
|
|
378
|
-
|
|
379
|
-
if (Number.
|
|
379
|
+
let threshold = Number(options.threshold);
|
|
380
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
380
381
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
if (options.batchSize !== undefined) {
|
|
384
|
-
|
|
385
|
-
if (!Number.
|
|
385
|
+
let n = Number(options.batchSize);
|
|
386
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
386
387
|
errors.push('Batch size must be a positive integer');
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
390
|
if (options.uploadTimeout !== undefined) {
|
|
390
|
-
|
|
391
|
-
if (!Number.
|
|
391
|
+
let n = Number(options.uploadTimeout);
|
|
392
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
392
393
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
393
394
|
}
|
|
394
395
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -3,16 +3,63 @@
|
|
|
3
3
|
* Shows current user and authentication status
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
|
|
7
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
8
|
-
import * as
|
|
6
|
+
import { createAuthClient as defaultCreateAuthClient, createTokenStore as defaultCreateTokenStore, getAuthTokens as defaultGetAuthTokens, whoami as defaultWhoami } from '../auth/index.js';
|
|
7
|
+
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
8
|
+
import * as defaultOutput from '../utils/output.js';
|
|
9
|
+
export function getTokenExpiryStatus(expiresAt, now = new Date()) {
|
|
10
|
+
let expiresDate = new Date(expiresAt);
|
|
11
|
+
let msUntilExpiry = expiresDate.getTime() - now.getTime();
|
|
12
|
+
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
13
|
+
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
14
|
+
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
15
|
+
if (msUntilExpiry <= 0) {
|
|
16
|
+
return {
|
|
17
|
+
level: 'warn',
|
|
18
|
+
message: 'Token has expired',
|
|
19
|
+
refreshHint: 'Run "vizzly login" to refresh your authentication'
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (daysUntilExpiry > 0) {
|
|
23
|
+
return {
|
|
24
|
+
level: 'hint',
|
|
25
|
+
message: `Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresDate.toLocaleDateString()})`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (hoursUntilExpiry > 0) {
|
|
29
|
+
return {
|
|
30
|
+
level: 'hint',
|
|
31
|
+
message: `Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (minutesUntilExpiry > 0) {
|
|
35
|
+
return {
|
|
36
|
+
level: 'hint',
|
|
37
|
+
message: `Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
level: 'warn',
|
|
42
|
+
message: 'Token expires in less than a minute',
|
|
43
|
+
refreshHint: 'Run "vizzly login" to refresh your authentication'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
9
46
|
|
|
10
47
|
/**
|
|
11
48
|
* Whoami command implementation
|
|
12
49
|
* @param {Object} options - Command options
|
|
13
50
|
* @param {Object} globalOptions - Global CLI options
|
|
51
|
+
* @param {Object} deps - Dependencies for testing
|
|
14
52
|
*/
|
|
15
|
-
export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
53
|
+
export async function whoamiCommand(options = {}, globalOptions = {}, deps = {}) {
|
|
54
|
+
let {
|
|
55
|
+
createAuthClient = defaultCreateAuthClient,
|
|
56
|
+
createTokenStore = defaultCreateTokenStore,
|
|
57
|
+
getApiUrl = defaultGetApiUrl,
|
|
58
|
+
getAuthTokens = defaultGetAuthTokens,
|
|
59
|
+
output = defaultOutput,
|
|
60
|
+
whoami = defaultWhoami,
|
|
61
|
+
exit = code => process.exit(code)
|
|
62
|
+
} = deps;
|
|
16
63
|
output.configure({
|
|
17
64
|
json: globalOptions.json,
|
|
18
65
|
verbose: globalOptions.verbose,
|
|
@@ -98,27 +145,17 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
98
145
|
// Show token expiry info
|
|
99
146
|
if (auth.expiresAt) {
|
|
100
147
|
output.blank();
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
105
|
-
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
106
|
-
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
107
|
-
if (msUntilExpiry <= 0) {
|
|
108
|
-
output.warn('Token has expired');
|
|
109
|
-
output.hint('Run "vizzly login" to refresh your authentication');
|
|
110
|
-
} else if (daysUntilExpiry > 0) {
|
|
111
|
-
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
|
|
112
|
-
} else if (hoursUntilExpiry > 0) {
|
|
113
|
-
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
|
|
114
|
-
} else if (minutesUntilExpiry > 0) {
|
|
115
|
-
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
|
|
148
|
+
let expiry = getTokenExpiryStatus(auth.expiresAt);
|
|
149
|
+
if (expiry.level === 'warn') {
|
|
150
|
+
output.warn(expiry.message);
|
|
116
151
|
} else {
|
|
117
|
-
output.
|
|
118
|
-
|
|
152
|
+
output.hint(expiry.message);
|
|
153
|
+
}
|
|
154
|
+
if (expiry.refreshHint) {
|
|
155
|
+
output.hint(expiry.refreshHint);
|
|
119
156
|
}
|
|
120
157
|
if (globalOptions.verbose) {
|
|
121
|
-
output.hint(`Token expires at: ${expiresAt.toISOString()}`);
|
|
158
|
+
output.hint(`Token expires at: ${new Date(auth.expiresAt).toISOString()}`);
|
|
122
159
|
}
|
|
123
160
|
}
|
|
124
161
|
output.cleanup();
|
|
@@ -138,10 +175,11 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
138
175
|
output.hint('Run "vizzly login" to authenticate again');
|
|
139
176
|
}
|
|
140
177
|
output.cleanup();
|
|
141
|
-
|
|
178
|
+
exit(1);
|
|
142
179
|
} else {
|
|
143
180
|
output.error('Failed to get user information', error);
|
|
144
|
-
|
|
181
|
+
output.cleanup();
|
|
182
|
+
exit(1);
|
|
145
183
|
}
|
|
146
184
|
}
|
|
147
185
|
}
|
|
@@ -151,7 +189,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
151
189
|
* @param {Object} options - Command options
|
|
152
190
|
*/
|
|
153
191
|
export function validateWhoamiOptions() {
|
|
154
|
-
|
|
192
|
+
let errors = [];
|
|
155
193
|
|
|
156
194
|
// No specific validation needed for whoami command
|
|
157
195
|
|
package/dist/config/core.js
CHANGED
|
@@ -29,7 +29,8 @@ export const CONFIG_DEFAULTS = {
|
|
|
29
29
|
timeout: 30000
|
|
30
30
|
},
|
|
31
31
|
comparison: {
|
|
32
|
-
threshold: 2.0
|
|
32
|
+
threshold: 2.0,
|
|
33
|
+
minClusterSize: 2
|
|
33
34
|
},
|
|
34
35
|
tdd: {
|
|
35
36
|
openReport: false
|
|
@@ -91,6 +92,23 @@ export function validateWriteScope(scope) {
|
|
|
91
92
|
// Deep Merge
|
|
92
93
|
// ============================================================================
|
|
93
94
|
|
|
95
|
+
function isPlainConfigObject(value) {
|
|
96
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function cloneConfigValue(value) {
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.map(cloneConfigValue);
|
|
101
|
+
}
|
|
102
|
+
if (isPlainConfigObject(value)) {
|
|
103
|
+
let cloned = {};
|
|
104
|
+
for (let key of Object.keys(value)) {
|
|
105
|
+
cloned[key] = cloneConfigValue(value[key]);
|
|
106
|
+
}
|
|
107
|
+
return cloned;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
/**
|
|
95
113
|
* Deep merge two objects
|
|
96
114
|
* @param {Object} target - Target object
|
|
@@ -98,18 +116,16 @@ export function validateWriteScope(scope) {
|
|
|
98
116
|
* @returns {Object} Merged object (new object, inputs not mutated)
|
|
99
117
|
*/
|
|
100
118
|
export function deepMerge(target, source) {
|
|
101
|
-
let output =
|
|
102
|
-
...target
|
|
103
|
-
};
|
|
119
|
+
let output = cloneConfigValue(target);
|
|
104
120
|
for (let key of Object.keys(source)) {
|
|
105
|
-
if (
|
|
106
|
-
if (
|
|
121
|
+
if (isPlainConfigObject(source[key])) {
|
|
122
|
+
if (isPlainConfigObject(target[key])) {
|
|
107
123
|
output[key] = deepMerge(target[key], source[key]);
|
|
108
124
|
} else {
|
|
109
|
-
output[key] = source[key];
|
|
125
|
+
output[key] = cloneConfigValue(source[key]);
|
|
110
126
|
}
|
|
111
127
|
} else {
|
|
112
|
-
output[key] = source[key];
|
|
128
|
+
output[key] = cloneConfigValue(source[key]);
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
return output;
|
|
@@ -125,11 +141,18 @@ export function deepMerge(target, source) {
|
|
|
125
141
|
* @returns {Object} The value if it's an object, empty object otherwise
|
|
126
142
|
*/
|
|
127
143
|
function ensureObject(value) {
|
|
128
|
-
if (
|
|
144
|
+
if (isPlainConfigObject(value)) {
|
|
129
145
|
return value;
|
|
130
146
|
}
|
|
131
147
|
return {};
|
|
132
148
|
}
|
|
149
|
+
function applyConfigLayer(config, sources, layer, sourceName) {
|
|
150
|
+
let merged = deepMerge(config, layer);
|
|
151
|
+
for (let key of Object.keys(layer)) {
|
|
152
|
+
sources[key] = sourceName;
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
133
156
|
|
|
134
157
|
/**
|
|
135
158
|
* Build merged config from layers with source tracking
|
|
@@ -148,36 +171,14 @@ export function buildMergedConfig({
|
|
|
148
171
|
let safeProjectConfig = ensureObject(projectConfig);
|
|
149
172
|
let safeGlobalConfig = ensureObject(globalConfig);
|
|
150
173
|
let safeEnvOverrides = ensureObject(envOverrides);
|
|
151
|
-
let mergedConfig =
|
|
174
|
+
let mergedConfig = cloneConfigValue(CONFIG_DEFAULTS);
|
|
152
175
|
let sources = {};
|
|
153
|
-
|
|
154
|
-
// Layer 1: Defaults
|
|
155
176
|
for (let key of Object.keys(CONFIG_DEFAULTS)) {
|
|
156
|
-
mergedConfig[key] = CONFIG_DEFAULTS[key];
|
|
157
177
|
sources[key] = 'default';
|
|
158
178
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
mergedConfig.auth = safeGlobalConfig.auth;
|
|
163
|
-
sources.auth = 'global';
|
|
164
|
-
}
|
|
165
|
-
if (safeGlobalConfig.projects) {
|
|
166
|
-
mergedConfig.projects = safeGlobalConfig.projects;
|
|
167
|
-
sources.projects = 'global';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Layer 3: Project config file
|
|
171
|
-
for (let key of Object.keys(safeProjectConfig)) {
|
|
172
|
-
mergedConfig[key] = safeProjectConfig[key];
|
|
173
|
-
sources[key] = 'project';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Layer 4: Environment variables
|
|
177
|
-
for (let key of Object.keys(safeEnvOverrides)) {
|
|
178
|
-
mergedConfig[key] = safeEnvOverrides[key];
|
|
179
|
-
sources[key] = 'env';
|
|
180
|
-
}
|
|
179
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeGlobalConfig, 'global');
|
|
180
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeProjectConfig, 'project');
|
|
181
|
+
mergedConfig = applyConfigLayer(mergedConfig, sources, safeEnvOverrides, 'env');
|
|
181
182
|
return {
|
|
182
183
|
config: mergedConfig,
|
|
183
184
|
sources
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { normalizeReportData } from '../utils/report-data.js';
|
|
4
5
|
let LOCAL_CONTEXT_SOURCE = 'local_workspace';
|
|
5
6
|
let DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT = 50;
|
|
6
7
|
function readJsonIfExists(path) {
|
|
7
|
-
if (!
|
|
8
|
+
if (!defaultExistsSync(path)) {
|
|
8
9
|
return null;
|
|
9
10
|
}
|
|
10
11
|
try {
|
|
@@ -72,7 +73,7 @@ function resolveAssetReference(assetPath, snapshot) {
|
|
|
72
73
|
if (!assetPath) {
|
|
73
74
|
return null;
|
|
74
75
|
}
|
|
75
|
-
if (/^https?:\/\//.test(assetPath)
|
|
76
|
+
if (/^https?:\/\//.test(assetPath)) {
|
|
76
77
|
return assetPath;
|
|
77
78
|
}
|
|
78
79
|
if (assetPath.startsWith('/images/')) {
|
|
@@ -81,6 +82,9 @@ function resolveAssetReference(assetPath, snapshot) {
|
|
|
81
82
|
}
|
|
82
83
|
return join(snapshot.vizzlyDir, assetPath.replace('/images/', ''));
|
|
83
84
|
}
|
|
85
|
+
if (isAbsolute(assetPath)) {
|
|
86
|
+
return assetPath;
|
|
87
|
+
}
|
|
84
88
|
return assetPath;
|
|
85
89
|
}
|
|
86
90
|
function normalizeConfirmedRegions(regions = []) {
|
|
@@ -149,6 +153,71 @@ function buildBuildSnapshot(snapshot) {
|
|
|
149
153
|
created_at: snapshot.session?.createdAt || null
|
|
150
154
|
};
|
|
151
155
|
}
|
|
156
|
+
function buildBaselineSnapshot(snapshot) {
|
|
157
|
+
let metadata = snapshot.baselineMetadata;
|
|
158
|
+
if (!metadata) {
|
|
159
|
+
return {
|
|
160
|
+
selected: null,
|
|
161
|
+
selection_reason: 'no_local_baseline_metadata',
|
|
162
|
+
comparison_baseline_build_ids: []
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
selected: {
|
|
167
|
+
id: metadata.buildId || 'local-baseline',
|
|
168
|
+
name: metadata.buildName || metadata.buildId || 'Local TDD Baseline',
|
|
169
|
+
branch: metadata.branch || 'local',
|
|
170
|
+
commit_sha: metadata.buildInfo?.commitSha || null,
|
|
171
|
+
commit_message: metadata.buildInfo?.commitMessage || null,
|
|
172
|
+
approval_status: metadata.buildInfo?.approvalStatus || 'approved',
|
|
173
|
+
status: metadata.buildInfo?.completedAt ? 'completed' : 'local',
|
|
174
|
+
created_at: metadata.createdAt || null,
|
|
175
|
+
completed_at: metadata.buildInfo?.completedAt || null
|
|
176
|
+
},
|
|
177
|
+
selection_reason: 'local_workspace_baseline_metadata',
|
|
178
|
+
comparison_baseline_build_ids: metadata.buildId ? [metadata.buildId] : []
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function buildReviewState(build, reviewSummary) {
|
|
182
|
+
let reasons = [];
|
|
183
|
+
if (build.approval_status === 'pending') {
|
|
184
|
+
reasons.push('build_pending_approval');
|
|
185
|
+
}
|
|
186
|
+
if (reviewSummary.pending > 0) {
|
|
187
|
+
reasons.push('comparisons_need_review');
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
needs_review: reasons.length > 0,
|
|
191
|
+
reasons,
|
|
192
|
+
pending_comparisons: reviewSummary.pending,
|
|
193
|
+
unresolved_comments: 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function mapLocalScreenshot(snapshot, comparison) {
|
|
197
|
+
let mapped = mapLocalComparison(snapshot, comparison);
|
|
198
|
+
let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
|
|
199
|
+
return {
|
|
200
|
+
id: mapped.screenshot.id,
|
|
201
|
+
name: mapped.screenshot.name,
|
|
202
|
+
browser: mapped.screenshot.browser,
|
|
203
|
+
viewport: {
|
|
204
|
+
width: mapped.screenshot.viewport_width,
|
|
205
|
+
height: mapped.screenshot.viewport_height
|
|
206
|
+
},
|
|
207
|
+
url: mapped.screenshot.original_url,
|
|
208
|
+
baseline: mapped.baseline ? {
|
|
209
|
+
id: mapped.baseline.id,
|
|
210
|
+
build_id: baselineBuildId,
|
|
211
|
+
name: mapped.baseline.name,
|
|
212
|
+
browser: mapped.baseline.browser,
|
|
213
|
+
viewport: {
|
|
214
|
+
width: mapped.baseline.viewport_width,
|
|
215
|
+
height: mapped.baseline.viewport_height
|
|
216
|
+
},
|
|
217
|
+
url: mapped.baseline.original_url
|
|
218
|
+
} : null
|
|
219
|
+
};
|
|
220
|
+
}
|
|
152
221
|
function mapLocalComparison(snapshot, comparison) {
|
|
153
222
|
let details = snapshot.comparisonDetails[comparison.id] || {};
|
|
154
223
|
let comparisonName = comparison.originalName || comparison.name;
|
|
@@ -156,12 +225,19 @@ function mapLocalComparison(snapshot, comparison) {
|
|
|
156
225
|
let hotspotAnalysis = buildHotspotAnalysis(snapshot, comparisonName, details);
|
|
157
226
|
let properties = comparison.properties || {};
|
|
158
227
|
let buildSnapshot = buildBuildSnapshot(snapshot);
|
|
228
|
+
let result = mapComparisonResult(comparison.status);
|
|
229
|
+
let approvalStatus = mapApprovalStatus(comparison.status);
|
|
230
|
+
let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
|
|
231
|
+
let diffImageUrl = resolveAssetReference(comparison.diff, snapshot);
|
|
232
|
+
let diffRegions = details.diffClusters || [];
|
|
159
233
|
return {
|
|
160
234
|
id: comparison.id,
|
|
161
235
|
name: comparisonName,
|
|
236
|
+
screenshot_name: comparisonName,
|
|
162
237
|
status: comparison.status,
|
|
163
|
-
result
|
|
164
|
-
approval_status:
|
|
238
|
+
result,
|
|
239
|
+
approval_status: approvalStatus,
|
|
240
|
+
needs_review: approvalStatus === 'pending' && ['changed', 'new'].includes(result),
|
|
165
241
|
build_id: buildSnapshot.id,
|
|
166
242
|
build_name: buildSnapshot.name,
|
|
167
243
|
build_branch: buildSnapshot.branch,
|
|
@@ -181,17 +257,33 @@ function mapLocalComparison(snapshot, comparison) {
|
|
|
181
257
|
},
|
|
182
258
|
baseline: comparison.baseline ? {
|
|
183
259
|
id: `${comparison.id}-baseline`,
|
|
260
|
+
build_id: baselineBuildId,
|
|
184
261
|
name: comparisonName,
|
|
185
262
|
browser: properties.browser ?? null,
|
|
186
263
|
viewport_width: properties.viewport_width ?? null,
|
|
187
264
|
viewport_height: properties.viewport_height ?? null,
|
|
188
265
|
original_url: resolveAssetReference(comparison.baseline, snapshot)
|
|
189
266
|
} : null,
|
|
267
|
+
diff: {
|
|
268
|
+
percentage: comparison.diffPercentage ?? null,
|
|
269
|
+
changed_pixels: comparison.diffCount ?? null,
|
|
270
|
+
total_pixels: comparison.totalPixels ?? null,
|
|
271
|
+
threshold: comparison.threshold ?? null,
|
|
272
|
+
image_url: diffImageUrl,
|
|
273
|
+
regions: diffRegions,
|
|
274
|
+
cluster_metadata: diffRegions.length ? {
|
|
275
|
+
clusterCount: diffRegions.length,
|
|
276
|
+
local_workspace: true
|
|
277
|
+
} : null,
|
|
278
|
+
fingerprint_hash: null,
|
|
279
|
+
fingerprint_data: null,
|
|
280
|
+
diff_lines: []
|
|
281
|
+
},
|
|
190
282
|
analysis: {
|
|
191
|
-
diff_image_url:
|
|
192
|
-
diff_regions:
|
|
193
|
-
cluster_metadata:
|
|
194
|
-
clusterCount:
|
|
283
|
+
diff_image_url: diffImageUrl,
|
|
284
|
+
diff_regions: diffRegions,
|
|
285
|
+
cluster_metadata: diffRegions.length ? {
|
|
286
|
+
clusterCount: diffRegions.length,
|
|
195
287
|
local_workspace: true
|
|
196
288
|
} : null,
|
|
197
289
|
diff_lines: null,
|
|
@@ -219,9 +311,17 @@ function createLocalWorkspaceError(message) {
|
|
|
219
311
|
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
220
312
|
return error;
|
|
221
313
|
}
|
|
314
|
+
function createReportUrl(snapshot) {
|
|
315
|
+
let reportPath = join(snapshot.vizzlyDir, 'report', 'index.html');
|
|
316
|
+
if (!snapshot.existsSync(reportPath)) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return pathToFileURL(reportPath).href;
|
|
320
|
+
}
|
|
222
321
|
export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
223
322
|
let projectRoot = options.projectRoot || process.cwd();
|
|
224
323
|
let readJson = deps.readJsonIfExists || readJsonIfExists;
|
|
324
|
+
let existsSync = deps.existsSync || defaultExistsSync;
|
|
225
325
|
let snapshotCache = null;
|
|
226
326
|
function loadSnapshot() {
|
|
227
327
|
if (snapshotCache) {
|
|
@@ -231,6 +331,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
231
331
|
snapshotCache = {
|
|
232
332
|
projectRoot,
|
|
233
333
|
vizzlyDir,
|
|
334
|
+
existsSync,
|
|
234
335
|
serverInfo: readJson(join(vizzlyDir, 'server.json')),
|
|
235
336
|
session: readJson(join(vizzlyDir, 'session.json')),
|
|
236
337
|
reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
|
|
@@ -278,14 +379,20 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
278
379
|
return buildLocalScope(projectRoot);
|
|
279
380
|
}
|
|
280
381
|
function createBuildLinks(snapshot) {
|
|
382
|
+
let reportUrl = createReportUrl(snapshot);
|
|
281
383
|
if (!snapshot.serverInfo?.port) {
|
|
282
|
-
return {
|
|
384
|
+
return reportUrl ? {
|
|
385
|
+
report_url: reportUrl
|
|
386
|
+
} : {};
|
|
283
387
|
}
|
|
284
388
|
let buildId = buildBuildSnapshot(snapshot).id;
|
|
285
389
|
return {
|
|
286
390
|
build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
|
|
287
391
|
comparison_url_prefix: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison`,
|
|
288
|
-
current_build_id: buildId
|
|
392
|
+
current_build_id: buildId,
|
|
393
|
+
...(reportUrl ? {
|
|
394
|
+
report_url: reportUrl
|
|
395
|
+
} : {})
|
|
289
396
|
};
|
|
290
397
|
}
|
|
291
398
|
function getBuildContext(buildId) {
|
|
@@ -295,25 +402,38 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
295
402
|
throw createLocalWorkspaceError(`Local workspace context is only available for the active session build (${resolvedBuild.id})`);
|
|
296
403
|
}
|
|
297
404
|
let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
|
|
405
|
+
let mappedScreenshots = snapshot.reportData.comparisons.map(comparison => mapLocalScreenshot(snapshot, comparison));
|
|
298
406
|
let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
|
|
407
|
+
let reviewState = buildReviewState(resolvedBuild, reviewSummary);
|
|
299
408
|
return {
|
|
300
409
|
resource: 'build_context',
|
|
301
410
|
source: LOCAL_CONTEXT_SOURCE,
|
|
302
411
|
scope: createScope(),
|
|
303
412
|
build: resolvedBuild,
|
|
413
|
+
baseline: buildBaselineSnapshot(snapshot),
|
|
414
|
+
status: reviewState,
|
|
304
415
|
summary: {
|
|
305
416
|
comparisons: {
|
|
306
417
|
total: mappedComparisons.length,
|
|
307
418
|
changed: mappedComparisons.filter(comparison => comparison.result === 'changed').length,
|
|
308
419
|
new: mappedComparisons.filter(comparison => comparison.result === 'new').length
|
|
309
420
|
},
|
|
310
|
-
review: reviewSummary
|
|
421
|
+
review: reviewSummary,
|
|
422
|
+
comments: {
|
|
423
|
+
build: 0,
|
|
424
|
+
screenshot: 0
|
|
425
|
+
}
|
|
311
426
|
},
|
|
312
427
|
review: {
|
|
313
428
|
comments: [],
|
|
314
429
|
assignments: []
|
|
315
430
|
},
|
|
431
|
+
screenshots: mappedScreenshots,
|
|
316
432
|
comparisons: mappedComparisons,
|
|
433
|
+
comments: {
|
|
434
|
+
build: [],
|
|
435
|
+
screenshot_count: 0
|
|
436
|
+
},
|
|
317
437
|
links: createBuildLinks(snapshot)
|
|
318
438
|
};
|
|
319
439
|
}
|
|
@@ -4,7 +4,8 @@ export function resolveContextSource(options = {}, deps = {}) {
|
|
|
4
4
|
requestedSource = 'auto',
|
|
5
5
|
command,
|
|
6
6
|
target = null,
|
|
7
|
-
projectRoot = process.cwd()
|
|
7
|
+
projectRoot = process.cwd(),
|
|
8
|
+
hasCloudScope = false
|
|
8
9
|
} = options;
|
|
9
10
|
let {
|
|
10
11
|
createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider
|
|
@@ -25,6 +26,9 @@ export function resolveContextSource(options = {}, deps = {}) {
|
|
|
25
26
|
}
|
|
26
27
|
return 'local';
|
|
27
28
|
}
|
|
29
|
+
if (hasCloudScope) {
|
|
30
|
+
return 'cloud';
|
|
31
|
+
}
|
|
28
32
|
if (isLocalAvailable && (localSnapshot == null ? localProvider.canHandle(command, target) : localProvider.canHandle(command, target, localSnapshot))) {
|
|
29
33
|
return 'local';
|
|
30
34
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,13 +11,14 @@ import 'dotenv/config';
|
|
|
11
11
|
// Client exports for convenience
|
|
12
12
|
export { configure, setEnabled, vizzlyScreenshot } from './client/index.js';
|
|
13
13
|
// Errors
|
|
14
|
-
export { UploadError } from './errors/vizzly-error.js';
|
|
14
|
+
export { AuthError, BuildError, ConfigError, NetworkError, ScreenshotError, TimeoutError, UploadError, ValidationError, VizzlyError } from './errors/vizzly-error.js';
|
|
15
|
+
export { createPluginServices } from './plugin-api.js';
|
|
15
16
|
// Primary SDK export
|
|
16
|
-
export { createVizzly } from './sdk/index.js';
|
|
17
|
+
export { createVizzly, VizzlySDK } from './sdk/index.js';
|
|
17
18
|
export { createServices } from './services/index.js';
|
|
18
|
-
// Core services (for advanced usage)
|
|
19
|
-
export { createUploader } from './services/uploader.js';
|
|
20
19
|
export { createTDDService } from './tdd/tdd-service.js';
|
|
20
|
+
// Core services (for advanced usage)
|
|
21
|
+
export { createUploader } from './uploader/index.js';
|
|
21
22
|
// Configuration helper
|
|
22
23
|
export { defineConfig } from './utils/config-helpers.js';
|
|
23
24
|
// Utilities
|