@vizzly-testing/cli 0.30.0 → 0.31.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/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/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- 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 +11 -8
- 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/uploader/index.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Uploader Module
|
|
3
3
|
*
|
|
4
|
-
* Exports pure functions (core)
|
|
4
|
+
* Exports pure functions (core), I/O operations, and the public uploader
|
|
5
|
+
* factory for screenshot uploading.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import { checkShas, createApiClient, createBuild } from '../api/index.js';
|
|
11
|
+
import { TimeoutError, UploadError, ValidationError } from '../errors/vizzly-error.js';
|
|
12
|
+
import { getDefaultBranch } from '../utils/git.js';
|
|
13
|
+
import * as output from '../utils/output.js';
|
|
14
|
+
import { resolveBatchSize, resolveTimeout } from './core.js';
|
|
15
|
+
import { upload as uploadOperation, waitForBuild as waitForBuildOperation } from './operations.js';
|
|
16
|
+
export function createUploader({
|
|
17
|
+
apiKey,
|
|
18
|
+
apiUrl,
|
|
19
|
+
userAgent,
|
|
20
|
+
command,
|
|
21
|
+
upload: uploadConfig = {}
|
|
22
|
+
} = {}, options = {}) {
|
|
23
|
+
let signal = options.signal || new AbortController().signal;
|
|
24
|
+
let client = createApiClient({
|
|
25
|
+
baseUrl: apiUrl,
|
|
26
|
+
token: apiKey,
|
|
27
|
+
command: command || 'upload',
|
|
28
|
+
sdkUserAgent: userAgent,
|
|
29
|
+
allowNoToken: true
|
|
30
|
+
});
|
|
31
|
+
let batchSize = resolveBatchSize(options, uploadConfig);
|
|
32
|
+
let timeout = resolveTimeout(options, uploadConfig);
|
|
33
|
+
let deps = options.deps || {
|
|
34
|
+
client,
|
|
35
|
+
createBuild,
|
|
36
|
+
getDefaultBranch,
|
|
37
|
+
glob,
|
|
38
|
+
readFile,
|
|
39
|
+
stat,
|
|
40
|
+
checkShas,
|
|
41
|
+
createError: (message, code, context) => {
|
|
42
|
+
let error = new UploadError(message, context);
|
|
43
|
+
error.code = code;
|
|
44
|
+
return error;
|
|
45
|
+
},
|
|
46
|
+
createValidationError: (message, context) => new ValidationError(message, context),
|
|
47
|
+
createUploadError: (message, context) => new UploadError(message, context),
|
|
48
|
+
createTimeoutError: (message, context) => new TimeoutError(message, context),
|
|
49
|
+
output
|
|
50
|
+
};
|
|
51
|
+
async function upload(uploadOptions) {
|
|
52
|
+
return uploadOperation({
|
|
53
|
+
uploadOptions,
|
|
54
|
+
config: {
|
|
55
|
+
apiKey,
|
|
56
|
+
apiUrl
|
|
57
|
+
},
|
|
58
|
+
signal,
|
|
59
|
+
batchSize,
|
|
60
|
+
deps: {
|
|
61
|
+
...deps,
|
|
62
|
+
client: deps.client || client
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function waitForBuild(buildId, waitTimeout = timeout) {
|
|
67
|
+
return waitForBuildOperation({
|
|
68
|
+
buildId,
|
|
69
|
+
timeout: waitTimeout,
|
|
70
|
+
signal,
|
|
71
|
+
client: deps.client || client,
|
|
72
|
+
deps: {
|
|
73
|
+
createError: deps.createError,
|
|
74
|
+
createTimeoutError: deps.createTimeoutError
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
upload,
|
|
80
|
+
waitForBuild
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
7
84
|
// Core - pure functions
|
|
8
|
-
export { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, computeSha256, DEFAULT_BATCH_SIZE, DEFAULT_SHA_CHECK_BATCH_SIZE, DEFAULT_TIMEOUT, extractBrowserFromFilename, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, resolveBatchSize, resolveTimeout, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
85
|
+
export { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, computeSha256, DEFAULT_BATCH_SIZE, DEFAULT_BUILD_POLL_INTERVAL, DEFAULT_SHA_CHECK_BATCH_SIZE, DEFAULT_TIMEOUT, extractBrowserFromFilename, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, resolveBatchSize, resolveTimeout, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
9
86
|
|
|
10
87
|
// Operations - I/O with dependency injection
|
|
11
88
|
export { checkExistingFiles, findScreenshots, processFiles, upload, uploadFiles, waitForBuild } from './operations.js';
|
|
@@ -4,7 +4,34 @@
|
|
|
4
4
|
* Each operation takes its dependencies as parameters for testability.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, DEFAULT_SHA_CHECK_BATCH_SIZE, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
7
|
+
import { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, DEFAULT_BUILD_POLL_INTERVAL, DEFAULT_SHA_CHECK_BATCH_SIZE, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
8
|
+
function delay(ms, signal) {
|
|
9
|
+
if (ms <= 0) {
|
|
10
|
+
return Promise.resolve();
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let timeoutId;
|
|
14
|
+
function cleanup() {
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
signal?.removeEventListener?.('abort', handleAbort);
|
|
17
|
+
}
|
|
18
|
+
function handleAbort() {
|
|
19
|
+
cleanup();
|
|
20
|
+
reject(new Error('Operation cancelled'));
|
|
21
|
+
}
|
|
22
|
+
timeoutId = setTimeout(() => {
|
|
23
|
+
cleanup();
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
handleAbort();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
signal?.addEventListener?.('abort', handleAbort, {
|
|
31
|
+
once: true
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
8
35
|
|
|
9
36
|
// ============================================================================
|
|
10
37
|
// File Discovery
|
|
@@ -228,10 +255,13 @@ export async function waitForBuild({
|
|
|
228
255
|
}) {
|
|
229
256
|
let {
|
|
230
257
|
createError,
|
|
231
|
-
createTimeoutError
|
|
258
|
+
createTimeoutError,
|
|
259
|
+
now = () => Date.now(),
|
|
260
|
+
pollInterval = DEFAULT_BUILD_POLL_INTERVAL,
|
|
261
|
+
wait = delay
|
|
232
262
|
} = deps;
|
|
233
|
-
let startTime =
|
|
234
|
-
while (!isTimedOut(startTime, timeout)) {
|
|
263
|
+
let startTime = now();
|
|
264
|
+
while (!isTimedOut(startTime, timeout, now())) {
|
|
235
265
|
if (signal.aborted) {
|
|
236
266
|
throw createError('Operation cancelled', 'UPLOAD_CANCELLED', {
|
|
237
267
|
buildId
|
|
@@ -253,11 +283,16 @@ export async function waitForBuild({
|
|
|
253
283
|
if (build.status === 'failed') {
|
|
254
284
|
throw createError(`Build failed: ${build.error || 'Unknown error'}`, 'BUILD_FAILED');
|
|
255
285
|
}
|
|
286
|
+
let remaining = timeout - getElapsedTime(startTime, now());
|
|
287
|
+
if (remaining <= 0) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
await wait(Math.min(pollInterval, remaining), signal);
|
|
256
291
|
}
|
|
257
292
|
throw createTimeoutError(`Build timed out after ${timeout}ms`, {
|
|
258
293
|
buildId,
|
|
259
294
|
timeout,
|
|
260
|
-
elapsed: getElapsedTime(startTime)
|
|
295
|
+
elapsed: getElapsedTime(startTime, now())
|
|
261
296
|
});
|
|
262
297
|
}
|
|
263
298
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function getAppBaseUrl(apiUrl) {
|
|
2
|
+
try {
|
|
3
|
+
let url = new URL(apiUrl);
|
|
4
|
+
let apiPathIndex = url.pathname.indexOf('/api');
|
|
5
|
+
if (apiPathIndex !== -1) {
|
|
6
|
+
url.pathname = url.pathname.slice(0, apiPathIndex) || '/';
|
|
7
|
+
url.search = '';
|
|
8
|
+
url.hash = '';
|
|
9
|
+
}
|
|
10
|
+
return url.toString().replace(/\/$/, '');
|
|
11
|
+
} catch {
|
|
12
|
+
return apiUrl.replace(/\/api(?:\/.*)?$/, '');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Race an operation against a timeout and always clear the timer when either
|
|
3
|
+
* side settles.
|
|
4
|
+
*
|
|
5
|
+
* @param {Promise<unknown>} operation - Operation promise to wait for.
|
|
6
|
+
* @param {number} ms - Timeout in milliseconds.
|
|
7
|
+
* @param {string} message - Error message when the timeout wins.
|
|
8
|
+
* @param {Object} [timers] - Timer implementation for deterministic tests.
|
|
9
|
+
* @returns {Promise<unknown>} The operation result.
|
|
10
|
+
*/
|
|
11
|
+
export async function withTimeout(operation, ms, message, timers = {
|
|
12
|
+
setTimeout,
|
|
13
|
+
clearTimeout
|
|
14
|
+
}) {
|
|
15
|
+
let timeoutId;
|
|
16
|
+
let timeoutPromise = new Promise((_, reject) => {
|
|
17
|
+
timeoutId = timers.setTimeout(() => reject(new Error(message)), ms);
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
21
|
+
} finally {
|
|
22
|
+
if (timeoutId !== undefined) {
|
|
23
|
+
timers.clearTimeout(timeoutId);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/utils/ci-env.js
CHANGED
|
@@ -176,6 +176,13 @@ export function getCommitMessage() {
|
|
|
176
176
|
// Generic
|
|
177
177
|
null;
|
|
178
178
|
}
|
|
179
|
+
function parsePullRequestNumber(value) {
|
|
180
|
+
if (!/^\d+$/.test(value)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
let number = Number(value);
|
|
184
|
+
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
185
|
+
}
|
|
179
186
|
|
|
180
187
|
/**
|
|
181
188
|
* Get the pull request number from CI environment variables
|
|
@@ -184,54 +191,54 @@ export function getCommitMessage() {
|
|
|
184
191
|
export function getPullRequestNumber() {
|
|
185
192
|
// Check VIZZLY override first
|
|
186
193
|
if (process.env.VIZZLY_PR_NUMBER) {
|
|
187
|
-
return
|
|
194
|
+
return parsePullRequestNumber(process.env.VIZZLY_PR_NUMBER);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
// GitHub Actions - extract from GITHUB_REF
|
|
191
198
|
if (process.env.GITHUB_ACTIONS && process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
192
|
-
|
|
193
|
-
if (prMatch?.[1]) return
|
|
199
|
+
let prMatch = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/);
|
|
200
|
+
if (prMatch?.[1]) return parsePullRequestNumber(prMatch[1]);
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
// GitLab CI
|
|
197
204
|
if (process.env.CI_MERGE_REQUEST_ID) {
|
|
198
|
-
return
|
|
205
|
+
return parsePullRequestNumber(process.env.CI_MERGE_REQUEST_ID);
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
// CircleCI - extract from PR URL
|
|
202
209
|
if (process.env.CIRCLE_PULL_REQUEST) {
|
|
203
|
-
|
|
204
|
-
if (prMatch?.[1]) return
|
|
210
|
+
let prMatch = process.env.CIRCLE_PULL_REQUEST.match(/\/pull\/(\d+)$/);
|
|
211
|
+
if (prMatch?.[1]) return parsePullRequestNumber(prMatch[1]);
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
// Travis CI
|
|
208
215
|
if (process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false') {
|
|
209
|
-
return
|
|
216
|
+
return parsePullRequestNumber(process.env.TRAVIS_PULL_REQUEST);
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
// Buildkite
|
|
213
220
|
if (process.env.BUILDKITE_PULL_REQUEST && process.env.BUILDKITE_PULL_REQUEST !== 'false') {
|
|
214
|
-
return
|
|
221
|
+
return parsePullRequestNumber(process.env.BUILDKITE_PULL_REQUEST);
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
// Drone CI
|
|
218
225
|
if (process.env.DRONE_PULL_REQUEST) {
|
|
219
|
-
return
|
|
226
|
+
return parsePullRequestNumber(process.env.DRONE_PULL_REQUEST);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
// Jenkins (GitHub Pull Request Builder plugin)
|
|
223
230
|
if (process.env.ghprbPullId) {
|
|
224
|
-
return
|
|
231
|
+
return parsePullRequestNumber(process.env.ghprbPullId);
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
// Azure DevOps
|
|
228
235
|
if (process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
|
|
229
|
-
return
|
|
236
|
+
return parsePullRequestNumber(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID);
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
// AppVeyor
|
|
233
240
|
if (process.env.APPVEYOR_PULL_REQUEST_NUMBER) {
|
|
234
|
-
return
|
|
241
|
+
return parsePullRequestNumber(process.env.APPVEYOR_PULL_REQUEST_NUMBER);
|
|
235
242
|
}
|
|
236
243
|
return null;
|
|
237
244
|
}
|
|
@@ -1,45 +1,15 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
|
+
import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
|
|
3
4
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
4
5
|
import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
|
|
5
6
|
import { getAccessToken } from './global-config.js';
|
|
6
7
|
import * as output from './output.js';
|
|
7
|
-
|
|
8
|
-
// API Configuration
|
|
9
|
-
apiKey: undefined,
|
|
10
|
-
// Will be set from env, global config, or CLI overrides
|
|
11
|
-
apiUrl: getApiUrl(),
|
|
12
|
-
// Server Configuration (for run command)
|
|
13
|
-
server: {
|
|
14
|
-
port: 47392,
|
|
15
|
-
timeout: 30000
|
|
16
|
-
},
|
|
17
|
-
// Build Configuration
|
|
18
|
-
build: {
|
|
19
|
-
name: 'Build {timestamp}',
|
|
20
|
-
environment: 'test'
|
|
21
|
-
},
|
|
22
|
-
// Upload Configuration (for upload command)
|
|
23
|
-
upload: {
|
|
24
|
-
screenshotsDir: './screenshots',
|
|
25
|
-
batchSize: 10,
|
|
26
|
-
timeout: 30000
|
|
27
|
-
},
|
|
28
|
-
// Comparison Configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
|
|
29
|
-
comparison: {
|
|
30
|
-
threshold: 2.0
|
|
31
|
-
},
|
|
32
|
-
// TDD Configuration
|
|
33
|
-
tdd: {
|
|
34
|
-
openReport: false // Whether to auto-open HTML report in browser
|
|
35
|
-
},
|
|
36
|
-
// Plugins
|
|
37
|
-
plugins: []
|
|
38
|
-
};
|
|
8
|
+
import { getActiveProjectLink } from './project-link-store.js';
|
|
39
9
|
export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
40
10
|
// 1. Load from config file using cosmiconfig
|
|
41
|
-
|
|
42
|
-
|
|
11
|
+
let explorer = cosmiconfigSync('vizzly');
|
|
12
|
+
let result = configPath ? explorer.load(configPath) : explorer.search();
|
|
43
13
|
let fileConfig = {};
|
|
44
14
|
if (result?.config) {
|
|
45
15
|
// Handle ESM default export (cosmiconfig wraps it in { default: {...} })
|
|
@@ -47,28 +17,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
47
17
|
}
|
|
48
18
|
|
|
49
19
|
// 2. Validate config file using Zod schema
|
|
50
|
-
|
|
20
|
+
let validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
|
|
51
21
|
|
|
52
22
|
// Create a proper clone of the default config to avoid shared object references
|
|
53
|
-
|
|
54
|
-
...DEFAULT_CONFIG,
|
|
55
|
-
server: {
|
|
56
|
-
...DEFAULT_CONFIG.server
|
|
57
|
-
},
|
|
58
|
-
build: {
|
|
59
|
-
...DEFAULT_CONFIG.build
|
|
60
|
-
},
|
|
61
|
-
upload: {
|
|
62
|
-
...DEFAULT_CONFIG.upload
|
|
63
|
-
},
|
|
64
|
-
comparison: {
|
|
65
|
-
...DEFAULT_CONFIG.comparison
|
|
66
|
-
},
|
|
67
|
-
tdd: {
|
|
68
|
-
...DEFAULT_CONFIG.tdd
|
|
69
|
-
},
|
|
70
|
-
plugins: [...DEFAULT_CONFIG.plugins]
|
|
71
|
-
};
|
|
23
|
+
let config = deepMerge(CONFIG_DEFAULTS, {});
|
|
72
24
|
|
|
73
25
|
// Merge validated file config
|
|
74
26
|
mergeConfig(config, validatedFileConfig);
|
|
@@ -95,16 +47,35 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
95
47
|
}
|
|
96
48
|
applyCLIOverrides(config, cliOverrides);
|
|
97
49
|
|
|
98
|
-
// 5. Fall back to
|
|
99
|
-
// This enables interactive commands (builds, comparisons, approve, etc.)
|
|
100
|
-
// to work without a project token when the user is logged in
|
|
50
|
+
// 5. Fall back to a linked project token for cloud upload commands.
|
|
101
51
|
if (!config.apiKey) {
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
52
|
+
let linkedProject = await getActiveProjectLink({
|
|
53
|
+
apiUrl: config.apiUrl
|
|
54
|
+
});
|
|
55
|
+
if (linkedProject?.token) {
|
|
56
|
+
config.apiKey = linkedProject.token;
|
|
57
|
+
config.linkedProject = {
|
|
58
|
+
account: linkedProject.account,
|
|
59
|
+
apiUrl: linkedProject.apiUrl,
|
|
60
|
+
organizationSlug: linkedProject.organizationSlug,
|
|
61
|
+
organizationName: linkedProject.organizationName,
|
|
62
|
+
projectSlug: linkedProject.projectSlug,
|
|
63
|
+
projectName: linkedProject.projectName,
|
|
64
|
+
tokenId: linkedProject.tokenId,
|
|
65
|
+
tokenPrefix: linkedProject.tokenPrefix,
|
|
66
|
+
expiresAt: linkedProject.expiresAt,
|
|
67
|
+
storage: linkedProject.storage
|
|
68
|
+
};
|
|
69
|
+
output.debug('config', 'using linked project token');
|
|
106
70
|
}
|
|
107
71
|
}
|
|
72
|
+
|
|
73
|
+
// 6. Keep user auth separate from upload credentials.
|
|
74
|
+
let userToken = await getAccessToken();
|
|
75
|
+
if (userToken) {
|
|
76
|
+
config.userToken = userToken;
|
|
77
|
+
output.debug('config', 'using user login for user-authenticated commands');
|
|
78
|
+
}
|
|
108
79
|
return config;
|
|
109
80
|
}
|
|
110
81
|
|
|
@@ -126,19 +97,22 @@ function applyCLIOverrides(config, cliOverrides = {}) {
|
|
|
126
97
|
if (cliOverrides.parallelId) config.parallelId = cliOverrides.parallelId;
|
|
127
98
|
|
|
128
99
|
// Server overrides
|
|
129
|
-
if (cliOverrides.port) config.server.port =
|
|
130
|
-
if (cliOverrides.timeout) config.server.timeout =
|
|
100
|
+
if (cliOverrides.port) config.server.port = Number(cliOverrides.port);
|
|
101
|
+
if (cliOverrides.timeout) config.server.timeout = Number(cliOverrides.timeout);
|
|
131
102
|
|
|
132
103
|
// Upload overrides
|
|
133
104
|
if (cliOverrides.batchSize !== undefined) {
|
|
134
|
-
config.upload.batchSize =
|
|
105
|
+
config.upload.batchSize = Number(cliOverrides.batchSize);
|
|
135
106
|
}
|
|
136
107
|
if (cliOverrides.uploadTimeout !== undefined) {
|
|
137
|
-
config.upload.timeout =
|
|
108
|
+
config.upload.timeout = Number(cliOverrides.uploadTimeout);
|
|
138
109
|
}
|
|
139
110
|
|
|
140
111
|
// Comparison overrides
|
|
141
112
|
if (cliOverrides.threshold !== undefined) config.comparison.threshold = cliOverrides.threshold;
|
|
113
|
+
if (cliOverrides.minClusterSize !== undefined) {
|
|
114
|
+
config.comparison.minClusterSize = Number(cliOverrides.minClusterSize);
|
|
115
|
+
}
|
|
142
116
|
|
|
143
117
|
// Baseline overrides
|
|
144
118
|
if (cliOverrides.baselineBuild) config.baselineBuildId = cliOverrides.baselineBuild;
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { CONFIG_DEFAULTS } from '../config/core.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Server configuration schema
|
|
10
11
|
*/
|
|
11
12
|
const serverSchema = z.object({
|
|
12
|
-
port: z.number().int().positive().default(
|
|
13
|
-
timeout: z.number().int().positive().default(
|
|
13
|
+
port: z.number().int().positive().default(CONFIG_DEFAULTS.server.port),
|
|
14
|
+
timeout: z.number().int().positive().default(CONFIG_DEFAULTS.server.timeout)
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Build configuration schema
|
|
18
19
|
*/
|
|
19
20
|
const buildSchema = z.object({
|
|
20
|
-
name: z.string().default(
|
|
21
|
-
environment: z.string().default(
|
|
21
|
+
name: z.string().default(CONFIG_DEFAULTS.build.name),
|
|
22
|
+
environment: z.string().default(CONFIG_DEFAULTS.build.environment),
|
|
22
23
|
branch: z.string().optional(),
|
|
23
24
|
commit: z.string().optional(),
|
|
24
25
|
message: z.string().optional()
|
|
@@ -28,9 +29,9 @@ const buildSchema = z.object({
|
|
|
28
29
|
* Upload configuration schema
|
|
29
30
|
*/
|
|
30
31
|
const uploadSchema = z.object({
|
|
31
|
-
screenshotsDir: z.union([z.string(), z.array(z.string())]).default(
|
|
32
|
-
batchSize: z.number().int().positive().default(
|
|
33
|
-
timeout: z.number().int().positive().default(
|
|
32
|
+
screenshotsDir: z.union([z.string(), z.array(z.string())]).default(CONFIG_DEFAULTS.upload.screenshotsDir),
|
|
33
|
+
batchSize: z.number().int().positive().default(CONFIG_DEFAULTS.upload.batchSize),
|
|
34
|
+
timeout: z.number().int().positive().default(CONFIG_DEFAULTS.upload.timeout)
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -39,15 +40,15 @@ const uploadSchema = z.object({
|
|
|
39
40
|
* minClusterSize: pixels (1 = exact)
|
|
40
41
|
*/
|
|
41
42
|
const comparisonSchema = z.object({
|
|
42
|
-
threshold: z.number().min(0).default(
|
|
43
|
-
minClusterSize: z.int().min(1).default(
|
|
43
|
+
threshold: z.number().min(0).default(CONFIG_DEFAULTS.comparison.threshold),
|
|
44
|
+
minClusterSize: z.int().min(1).default(CONFIG_DEFAULTS.comparison.minClusterSize)
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* TDD configuration schema
|
|
48
49
|
*/
|
|
49
50
|
const tddSchema = z.object({
|
|
50
|
-
openReport: z.boolean().default(
|
|
51
|
+
openReport: z.boolean().default(CONFIG_DEFAULTS.tdd.openReport)
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -57,29 +58,14 @@ const tddSchema = z.object({
|
|
|
57
58
|
export const vizzlyConfigSchema = z.object({
|
|
58
59
|
// Core Vizzly config
|
|
59
60
|
apiKey: z.string().optional(),
|
|
60
|
-
apiUrl: z.string().url().
|
|
61
|
-
server: serverSchema.default(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: 'Build {timestamp}',
|
|
67
|
-
environment: 'test'
|
|
68
|
-
}),
|
|
69
|
-
upload: uploadSchema.default({
|
|
70
|
-
screenshotsDir: './screenshots',
|
|
71
|
-
batchSize: 10,
|
|
72
|
-
timeout: 30000
|
|
73
|
-
}),
|
|
74
|
-
comparison: comparisonSchema.default({
|
|
75
|
-
threshold: 2.0,
|
|
76
|
-
minClusterSize: 2
|
|
77
|
-
}),
|
|
78
|
-
tdd: tddSchema.default({
|
|
79
|
-
openReport: false
|
|
80
|
-
}),
|
|
61
|
+
apiUrl: z.string().url().default(CONFIG_DEFAULTS.apiUrl),
|
|
62
|
+
server: serverSchema.default(CONFIG_DEFAULTS.server),
|
|
63
|
+
build: buildSchema.default(CONFIG_DEFAULTS.build),
|
|
64
|
+
upload: uploadSchema.default(CONFIG_DEFAULTS.upload),
|
|
65
|
+
comparison: comparisonSchema.default(CONFIG_DEFAULTS.comparison),
|
|
66
|
+
tdd: tddSchema.default(CONFIG_DEFAULTS.tdd),
|
|
81
67
|
signatureProperties: z.array(z.string()).default([]),
|
|
82
|
-
plugins: z.array(z.string()).default(
|
|
68
|
+
plugins: z.array(z.string()).default(CONFIG_DEFAULTS.plugins),
|
|
83
69
|
// Additional optional fields
|
|
84
70
|
parallelId: z.string().optional(),
|
|
85
71
|
baselineBuildId: z.string().optional(),
|
|
@@ -88,29 +74,7 @@ export const vizzlyConfigSchema = z.object({
|
|
|
88
74
|
wait: z.boolean().optional(),
|
|
89
75
|
allowNoToken: z.boolean().optional()
|
|
90
76
|
}).passthrough() // Allow plugin-specific keys like `staticSite`, `storybook`, etc.
|
|
91
|
-
.default(
|
|
92
|
-
server: {
|
|
93
|
-
port: 47392,
|
|
94
|
-
timeout: 30000
|
|
95
|
-
},
|
|
96
|
-
build: {
|
|
97
|
-
name: 'Build {timestamp}',
|
|
98
|
-
environment: 'test'
|
|
99
|
-
},
|
|
100
|
-
upload: {
|
|
101
|
-
screenshotsDir: './screenshots',
|
|
102
|
-
batchSize: 10,
|
|
103
|
-
timeout: 30000
|
|
104
|
-
},
|
|
105
|
-
comparison: {
|
|
106
|
-
threshold: 2.0,
|
|
107
|
-
minClusterSize: 2
|
|
108
|
-
},
|
|
109
|
-
tdd: {
|
|
110
|
-
openReport: false
|
|
111
|
-
},
|
|
112
|
-
plugins: []
|
|
113
|
-
});
|
|
77
|
+
.default(CONFIG_DEFAULTS);
|
|
114
78
|
|
|
115
79
|
/**
|
|
116
80
|
* Validate Vizzly configuration
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
let defaultTimers = {
|
|
2
|
+
setTimeout,
|
|
3
|
+
clearTimeout
|
|
4
|
+
};
|
|
5
|
+
function fetchWithTimeout(url, opts = {}, ms = 300000, deps = {}) {
|
|
6
|
+
let fetchFn = deps.fetch || fetch;
|
|
7
|
+
let AbortControllerClass = deps.AbortController || AbortController;
|
|
8
|
+
let timers = deps.timers || defaultTimers;
|
|
9
|
+
let ctrl = new AbortControllerClass();
|
|
10
|
+
let timeoutId = timers.setTimeout(() => ctrl.abort(), ms);
|
|
11
|
+
return fetchFn(url, {
|
|
5
12
|
...opts,
|
|
6
13
|
signal: ctrl.signal
|
|
7
|
-
}).finally(() => clearTimeout(
|
|
14
|
+
}).finally(() => timers.clearTimeout(timeoutId));
|
|
8
15
|
}
|
|
9
16
|
export { fetchWithTimeout };
|