@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
@@ -10,9 +10,13 @@ import { join } from 'node:path';
|
|
|
10
10
|
* Enables the menubar app to discover and manage multiple concurrent servers.
|
|
11
11
|
*/
|
|
12
12
|
export class ServerRegistry {
|
|
13
|
-
constructor(
|
|
14
|
-
|
|
13
|
+
constructor({
|
|
14
|
+
vizzlyHome,
|
|
15
|
+
logger = console
|
|
16
|
+
} = {}) {
|
|
17
|
+
this.vizzlyHome = vizzlyHome || process.env.VIZZLY_HOME || join(homedir(), '.vizzly');
|
|
15
18
|
this.registryPath = join(this.vizzlyHome, 'servers.json');
|
|
19
|
+
this.logger = logger;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -40,7 +44,7 @@ export class ServerRegistry {
|
|
|
40
44
|
}
|
|
41
45
|
} catch (_err) {
|
|
42
46
|
// Corrupted file, start fresh
|
|
43
|
-
|
|
47
|
+
this.logger.warn('Warning: Could not read server registry, starting fresh');
|
|
44
48
|
}
|
|
45
49
|
return {
|
|
46
50
|
version: 1,
|
|
@@ -64,10 +68,10 @@ export class ServerRegistry {
|
|
|
64
68
|
if (!serverInfo.pid || !serverInfo.port || !serverInfo.directory) {
|
|
65
69
|
throw new Error('Missing required fields: pid, port, directory');
|
|
66
70
|
}
|
|
67
|
-
let port =
|
|
68
|
-
let pid =
|
|
69
|
-
if (
|
|
70
|
-
throw new Error('Invalid port or pid - must be
|
|
71
|
+
let port = parsePositiveInteger(serverInfo.port);
|
|
72
|
+
let pid = parsePositiveInteger(serverInfo.pid);
|
|
73
|
+
if (port === null || pid === null) {
|
|
74
|
+
throw new Error('Invalid port or pid - must be positive integers');
|
|
71
75
|
}
|
|
72
76
|
let registry = this.read();
|
|
73
77
|
|
|
@@ -171,15 +175,7 @@ export class ServerRegistry {
|
|
|
171
175
|
* The menubar app listens for this in addition to file watching.
|
|
172
176
|
*/
|
|
173
177
|
notifyMenubar() {
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
execSync('notifyutil -p dev.vizzly.serverChanged', {
|
|
177
|
-
stdio: 'ignore',
|
|
178
|
-
timeout: 500
|
|
179
|
-
});
|
|
180
|
-
} catch {
|
|
181
|
-
// Non-fatal - menubar will still see changes via file watching
|
|
182
|
-
}
|
|
178
|
+
notifyServerRegistryChanged();
|
|
183
179
|
}
|
|
184
180
|
|
|
185
181
|
/**
|
|
@@ -218,6 +214,31 @@ export class ServerRegistry {
|
|
|
218
214
|
return startPort;
|
|
219
215
|
}
|
|
220
216
|
}
|
|
217
|
+
function parsePositiveInteger(value) {
|
|
218
|
+
if (typeof value === 'number') {
|
|
219
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) {
|
|
222
|
+
return Number(value);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
export function notifyServerRegistryChanged({
|
|
227
|
+
platform = process.platform,
|
|
228
|
+
execFile = execFileSync
|
|
229
|
+
} = {}) {
|
|
230
|
+
if (platform !== 'darwin') return false;
|
|
231
|
+
try {
|
|
232
|
+
execFile('notifyutil', ['-p', 'dev.vizzly.serverChanged'], {
|
|
233
|
+
stdio: 'ignore',
|
|
234
|
+
timeout: 500
|
|
235
|
+
});
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
// Non-fatal - menubar will still see changes via file watching
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
221
242
|
|
|
222
243
|
/**
|
|
223
244
|
* Check if a port is free (not in use by any process)
|
|
@@ -57,6 +57,29 @@ export function clearBaselineData(paths) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Clear per-run screenshot artifacts while preserving approved baselines.
|
|
62
|
+
*
|
|
63
|
+
* @param {{ currentPath: string, diffPath: string }} paths
|
|
64
|
+
*/
|
|
65
|
+
export function clearRunData(paths) {
|
|
66
|
+
let {
|
|
67
|
+
currentPath,
|
|
68
|
+
diffPath
|
|
69
|
+
} = paths;
|
|
70
|
+
for (let dir of [currentPath, diffPath]) {
|
|
71
|
+
if (existsSync(dir)) {
|
|
72
|
+
rmSync(dir, {
|
|
73
|
+
recursive: true,
|
|
74
|
+
force: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(dir, {
|
|
78
|
+
recursive: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Save an image as baseline
|
|
62
85
|
*
|
|
@@ -32,6 +32,9 @@ export async function compareImages(baselinePath, currentPath, diffPath, options
|
|
|
32
32
|
diffPath,
|
|
33
33
|
overwrite: true,
|
|
34
34
|
includeClusters: true,
|
|
35
|
+
includeSSIM: true,
|
|
36
|
+
includeGMSD: true,
|
|
37
|
+
clusterMerge: true,
|
|
35
38
|
minClusterSize
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -141,6 +144,7 @@ export function buildFailedComparison(params) {
|
|
|
141
144
|
regionData
|
|
142
145
|
} = params;
|
|
143
146
|
let diffClusters = honeydiffResult.diffClusters || [];
|
|
147
|
+
let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
|
|
144
148
|
let isFiltered = false;
|
|
145
149
|
let filterReason = 'pixel-diff';
|
|
146
150
|
|
|
@@ -150,7 +154,9 @@ export function buildFailedComparison(params) {
|
|
|
150
154
|
let confirmedRegions = regionData?.confirmed || [];
|
|
151
155
|
if (confirmedRegions.length > 0 && diffClusters.length > 0) {
|
|
152
156
|
regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
|
|
153
|
-
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage
|
|
157
|
+
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
|
|
158
|
+
ssimScore
|
|
159
|
+
})) {
|
|
154
160
|
isRegionFiltered = true;
|
|
155
161
|
isFiltered = true;
|
|
156
162
|
filterReason = 'region-filtered';
|
|
@@ -198,8 +204,11 @@ export function buildFailedComparison(params) {
|
|
|
198
204
|
coverage: regionCoverage.coverage,
|
|
199
205
|
clustersInRegions: regionCoverage.clustersInRegions,
|
|
200
206
|
totalClusters: regionCoverage.totalClusters,
|
|
207
|
+
pixelsInRegions: regionCoverage.pixelsInRegions,
|
|
208
|
+
totalPixels: regionCoverage.totalPixels,
|
|
201
209
|
matchedRegions: regionCoverage.matchedRegions,
|
|
202
210
|
confirmedCount: confirmedRegions.length,
|
|
211
|
+
ssimScore,
|
|
203
212
|
isFiltered: isRegionFiltered
|
|
204
213
|
} : null,
|
|
205
214
|
// Include confirmed regions for visualization in UI
|
|
@@ -55,17 +55,4 @@ export async function downloadHotspots(options) {
|
|
|
55
55
|
error: error.message
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Extract screenshot names from a list of screenshots
|
|
62
|
-
*
|
|
63
|
-
* @param {Array} screenshots - Screenshots with name property
|
|
64
|
-
* @returns {string[]}
|
|
65
|
-
*/
|
|
66
|
-
export function extractScreenshotNames(screenshots) {
|
|
67
|
-
if (!screenshots || !Array.isArray(screenshots)) {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
return screenshots.map(s => s.name).filter(Boolean);
|
|
71
58
|
}
|
package/dist/tdd/tdd-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
12
|
-
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
12
|
+
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getComparisonContext as defaultGetComparisonContext, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
13
13
|
import { NetworkError } from '../errors/vizzly-error.js';
|
|
14
14
|
import { colors as defaultColors } from '../utils/colors.js';
|
|
15
15
|
import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
|
|
@@ -22,7 +22,8 @@ import { generateBaselineFilename as defaultGenerateBaselineFilename, generateCo
|
|
|
22
22
|
import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, loadBaselineMetadata as defaultLoadBaselineMetadata, saveBaselineMetadata as defaultSaveBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from './metadata/baseline-metadata.js';
|
|
23
23
|
import { loadHotspotMetadata as defaultLoadHotspotMetadata, saveHotspotMetadata as defaultSaveHotspotMetadata } from './metadata/hotspot-metadata.js';
|
|
24
24
|
import { loadRegionMetadata as defaultLoadRegionMetadata, saveRegionMetadata as defaultSaveRegionMetadata } from './metadata/region-metadata.js';
|
|
25
|
-
import {
|
|
25
|
+
import { buildTddDependencyOps, resolveTddPaths, resolveTddWorkingDirectory } from './runtime-context.js';
|
|
26
|
+
import { baselineExists as defaultBaselineExists, clearBaselineData as defaultClearBaselineData, clearRunData as defaultClearRunData, getBaselinePath as defaultGetBaselinePath, getCurrentPath as defaultGetCurrentPath, getDiffPath as defaultGetDiffPath, initializeDirectories as defaultInitializeDirectories, saveBaseline as defaultSaveBaseline, saveCurrent as defaultSaveCurrent } from './services/baseline-manager.js';
|
|
26
27
|
import { buildErrorComparison as defaultBuildErrorComparison, buildFailedComparison as defaultBuildFailedComparison, buildNewComparison as defaultBuildNewComparison, buildPassedComparison as defaultBuildPassedComparison, compareImages as defaultCompareImages, isDimensionMismatchError as defaultIsDimensionMismatchError } from './services/comparison-service.js';
|
|
27
28
|
import { buildResults as defaultBuildResults, getFailedComparisons as defaultGetFailedComparisons, getNewComparisons as defaultGetNewComparisons } from './services/result-service.js';
|
|
28
29
|
|
|
@@ -40,110 +41,80 @@ export function createTDDService(config, options = {}, deps = {}) {
|
|
|
40
41
|
}
|
|
41
42
|
export class TddService {
|
|
42
43
|
constructor(config, workingDir = process.cwd(), setBaseline = false, authService = null, deps = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
let comparisonOps = {
|
|
108
|
-
compareImages: defaultCompareImages,
|
|
109
|
-
buildPassedComparison: defaultBuildPassedComparison,
|
|
110
|
-
buildNewComparison: defaultBuildNewComparison,
|
|
111
|
-
buildFailedComparison: defaultBuildFailedComparison,
|
|
112
|
-
buildErrorComparison: defaultBuildErrorComparison,
|
|
113
|
-
isDimensionMismatchError: defaultIsDimensionMismatchError,
|
|
114
|
-
...comparison
|
|
115
|
-
};
|
|
116
|
-
let signatureOps = {
|
|
117
|
-
generateScreenshotSignature: defaultGenerateScreenshotSignature,
|
|
118
|
-
generateBaselineFilename: defaultGenerateBaselineFilename,
|
|
119
|
-
generateComparisonId: defaultGenerateComparisonId,
|
|
120
|
-
sanitizeScreenshotName: defaultSanitizeScreenshotName,
|
|
121
|
-
validateScreenshotProperties: defaultValidateScreenshotProperties,
|
|
122
|
-
safePath: defaultSafePath,
|
|
123
|
-
...signature
|
|
124
|
-
};
|
|
125
|
-
let resultsOps = {
|
|
126
|
-
buildResults: defaultBuildResults,
|
|
127
|
-
getFailedComparisons: defaultGetFailedComparisons,
|
|
128
|
-
getNewComparisons: defaultGetNewComparisons,
|
|
129
|
-
...results
|
|
44
|
+
let defaults = {
|
|
45
|
+
output: defaultOutput,
|
|
46
|
+
colors: defaultColors,
|
|
47
|
+
validatePathSecurity: defaultValidatePathSecurity,
|
|
48
|
+
initializeDirectories: defaultInitializeDirectories,
|
|
49
|
+
calculateHotspotCoverage: defaultCalculateHotspotCoverage,
|
|
50
|
+
fs: {
|
|
51
|
+
existsSync: defaultExistsSync,
|
|
52
|
+
mkdirSync: defaultMkdirSync,
|
|
53
|
+
readFileSync: defaultReadFileSync,
|
|
54
|
+
writeFileSync: defaultWriteFileSync
|
|
55
|
+
},
|
|
56
|
+
api: {
|
|
57
|
+
createApiClient: defaultCreateApiClient,
|
|
58
|
+
getTddBaselines: defaultGetTddBaselines,
|
|
59
|
+
getBuilds: defaultGetBuilds,
|
|
60
|
+
getComparison: defaultGetComparison,
|
|
61
|
+
getComparisonContext: defaultGetComparisonContext,
|
|
62
|
+
getBatchHotspots: defaultGetBatchHotspots,
|
|
63
|
+
fetchWithTimeout: defaultFetchWithTimeout,
|
|
64
|
+
getDefaultBranch: defaultGetDefaultBranch
|
|
65
|
+
},
|
|
66
|
+
metadata: {
|
|
67
|
+
loadBaselineMetadata: defaultLoadBaselineMetadata,
|
|
68
|
+
saveBaselineMetadata: defaultSaveBaselineMetadata,
|
|
69
|
+
createEmptyBaselineMetadata: defaultCreateEmptyBaselineMetadata,
|
|
70
|
+
upsertScreenshotInMetadata: defaultUpsertScreenshotInMetadata,
|
|
71
|
+
loadHotspotMetadata: defaultLoadHotspotMetadata,
|
|
72
|
+
saveHotspotMetadata: defaultSaveHotspotMetadata,
|
|
73
|
+
loadRegionMetadata: defaultLoadRegionMetadata,
|
|
74
|
+
saveRegionMetadata: defaultSaveRegionMetadata
|
|
75
|
+
},
|
|
76
|
+
baseline: {
|
|
77
|
+
baselineExists: defaultBaselineExists,
|
|
78
|
+
clearBaselineData: defaultClearBaselineData,
|
|
79
|
+
clearRunData: defaultClearRunData,
|
|
80
|
+
getBaselinePath: defaultGetBaselinePath,
|
|
81
|
+
getCurrentPath: defaultGetCurrentPath,
|
|
82
|
+
getDiffPath: defaultGetDiffPath,
|
|
83
|
+
saveBaseline: defaultSaveBaseline,
|
|
84
|
+
saveCurrent: defaultSaveCurrent
|
|
85
|
+
},
|
|
86
|
+
comparison: {
|
|
87
|
+
compareImages: defaultCompareImages,
|
|
88
|
+
buildPassedComparison: defaultBuildPassedComparison,
|
|
89
|
+
buildNewComparison: defaultBuildNewComparison,
|
|
90
|
+
buildFailedComparison: defaultBuildFailedComparison,
|
|
91
|
+
buildErrorComparison: defaultBuildErrorComparison,
|
|
92
|
+
isDimensionMismatchError: defaultIsDimensionMismatchError
|
|
93
|
+
},
|
|
94
|
+
signature: {
|
|
95
|
+
generateScreenshotSignature: defaultGenerateScreenshotSignature,
|
|
96
|
+
generateBaselineFilename: defaultGenerateBaselineFilename,
|
|
97
|
+
generateComparisonId: defaultGenerateComparisonId,
|
|
98
|
+
sanitizeScreenshotName: defaultSanitizeScreenshotName,
|
|
99
|
+
validateScreenshotProperties: defaultValidateScreenshotProperties,
|
|
100
|
+
safePath: defaultSafePath
|
|
101
|
+
},
|
|
102
|
+
results: {
|
|
103
|
+
buildResults: defaultBuildResults,
|
|
104
|
+
getFailedComparisons: defaultGetFailedComparisons,
|
|
105
|
+
getNewComparisons: defaultGetNewComparisons
|
|
106
|
+
}
|
|
130
107
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
108
|
+
let {
|
|
109
|
+
runtimeDeps,
|
|
110
|
+
apiOps
|
|
111
|
+
} = buildTddDependencyOps(deps, defaults);
|
|
112
|
+
let {
|
|
134
113
|
output,
|
|
135
|
-
colors,
|
|
136
114
|
validatePathSecurity,
|
|
137
|
-
initializeDirectories
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
...apiOps,
|
|
141
|
-
...metadataOps,
|
|
142
|
-
...baselineOps,
|
|
143
|
-
...comparisonOps,
|
|
144
|
-
...signatureOps,
|
|
145
|
-
...resultsOps
|
|
146
|
-
};
|
|
115
|
+
initializeDirectories
|
|
116
|
+
} = runtimeDeps;
|
|
117
|
+
this._deps = runtimeDeps;
|
|
147
118
|
this.config = config;
|
|
148
119
|
this.setBaseline = setBaseline;
|
|
149
120
|
this.authService = authService;
|
|
@@ -155,15 +126,8 @@ export class TddService {
|
|
|
155
126
|
});
|
|
156
127
|
|
|
157
128
|
// Validate and secure the working directory
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
} catch (error) {
|
|
161
|
-
output.error(`Invalid working directory: ${error.message}`);
|
|
162
|
-
throw new Error(`Working directory validation failed: ${error.message}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Initialize directories using extracted module
|
|
166
|
-
let paths = initializeDirectories(this.workingDir);
|
|
129
|
+
this.workingDir = resolveTddWorkingDirectory(workingDir, validatePathSecurity, output);
|
|
130
|
+
let paths = resolveTddPaths(this.workingDir, initializeDirectories);
|
|
167
131
|
this.baselinePath = paths.baselinePath;
|
|
168
132
|
this.currentPath = paths.currentPath;
|
|
169
133
|
this.diffPath = paths.diffPath;
|
|
@@ -171,7 +135,7 @@ export class TddService {
|
|
|
171
135
|
// State
|
|
172
136
|
this.baselineData = null;
|
|
173
137
|
this.comparisons = [];
|
|
174
|
-
this.threshold = config.comparison?.threshold
|
|
138
|
+
this.threshold = config.comparison?.threshold ?? 2.0;
|
|
175
139
|
this.minClusterSize = config.comparison?.minClusterSize ?? 2;
|
|
176
140
|
this.signatureProperties = config.signatureProperties ?? [];
|
|
177
141
|
|
|
@@ -187,6 +151,12 @@ export class TddService {
|
|
|
187
151
|
output.info('[vizzly] Baseline update mode - will overwrite existing baselines with new ones');
|
|
188
152
|
}
|
|
189
153
|
}
|
|
154
|
+
clearRunData() {
|
|
155
|
+
this._deps.clearRunData({
|
|
156
|
+
currentPath: this.currentPath,
|
|
157
|
+
diffPath: this.diffPath
|
|
158
|
+
});
|
|
159
|
+
}
|
|
190
160
|
|
|
191
161
|
/**
|
|
192
162
|
* Download baselines from cloud
|
|
@@ -200,6 +170,7 @@ export class TddService {
|
|
|
200
170
|
getTddBaselines,
|
|
201
171
|
getBuilds,
|
|
202
172
|
getComparison,
|
|
173
|
+
getComparisonContext,
|
|
203
174
|
clearBaselineData,
|
|
204
175
|
generateScreenshotSignature,
|
|
205
176
|
generateBaselineFilename,
|
|
@@ -252,10 +223,19 @@ export class TddService {
|
|
|
252
223
|
output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
|
|
253
224
|
}
|
|
254
225
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
226
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
255
227
|
} else if (comparisonId) {
|
|
256
228
|
// Handle specific comparison download
|
|
257
229
|
output.info(`Using comparison: ${comparisonId}`);
|
|
258
230
|
let comparison = await getComparison(this.client, comparisonId);
|
|
231
|
+
let comparisonContext = null;
|
|
232
|
+
if (!comparison.dynamic_content && getComparisonContext) {
|
|
233
|
+
try {
|
|
234
|
+
comparisonContext = await getComparisonContext(this.client, comparisonId);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
output.debug('tdd', `comparison context unavailable for ${comparisonId}: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
259
239
|
if (!comparison.baseline_screenshot) {
|
|
260
240
|
throw new Error(`Comparison ${comparisonId} has no baseline screenshot. This comparison may be a "new" screenshot.`);
|
|
261
241
|
}
|
|
@@ -286,6 +266,11 @@ export class TddService {
|
|
|
286
266
|
}
|
|
287
267
|
}
|
|
288
268
|
let screenshotName = comparison.baseline_name || comparison.current_name;
|
|
269
|
+
let dynamicContent = comparison.dynamic_content || {
|
|
270
|
+
hotspot_analysis: comparisonContext?.history?.hotspot_analysis,
|
|
271
|
+
confirmed_regions: comparisonContext?.history?.confirmed_regions
|
|
272
|
+
};
|
|
273
|
+
this.saveDynamicMetadataForScreenshot(screenshotName, dynamicContent);
|
|
289
274
|
let signature = generateScreenshotSignature(screenshotName, screenshotProperties, this.signatureProperties);
|
|
290
275
|
let filename = generateBaselineFilename(screenshotName, signature);
|
|
291
276
|
baselineBuild = {
|
|
@@ -325,16 +310,7 @@ export class TddService {
|
|
|
325
310
|
}
|
|
326
311
|
baselineBuild = apiResponse.build;
|
|
327
312
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
328
|
-
|
|
329
|
-
// Store bundled hotspots and regions from API response
|
|
330
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
331
|
-
this.hotspotData = apiResponse.hotspots;
|
|
332
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
333
|
-
}
|
|
334
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
335
|
-
this.regionData = apiResponse.regions;
|
|
336
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
337
|
-
}
|
|
313
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
338
314
|
}
|
|
339
315
|
let buildDetails = baselineBuild;
|
|
340
316
|
if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
|
|
@@ -519,6 +495,49 @@ export class TddService {
|
|
|
519
495
|
throw error;
|
|
520
496
|
}
|
|
521
497
|
}
|
|
498
|
+
saveDynamicMetadataFromBaselineResponse(apiResponse = {}) {
|
|
499
|
+
let {
|
|
500
|
+
saveHotspotMetadata,
|
|
501
|
+
saveRegionMetadata
|
|
502
|
+
} = this._deps;
|
|
503
|
+
let hasHotspots = Object.keys(apiResponse.hotspots || {}).length > 0;
|
|
504
|
+
let hasRegions = Object.keys(apiResponse.regions || {}).length > 0;
|
|
505
|
+
if (hasHotspots) {
|
|
506
|
+
this.hotspotData = apiResponse.hotspots;
|
|
507
|
+
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
508
|
+
}
|
|
509
|
+
if (hasRegions) {
|
|
510
|
+
this.regionData = apiResponse.regions;
|
|
511
|
+
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
saveDynamicMetadataForScreenshot(screenshotName, dynamicContent = {}) {
|
|
515
|
+
if (!screenshotName) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
let hotspotAnalysis = dynamicContent.hotspot_analysis;
|
|
519
|
+
let confirmedRegions = dynamicContent.confirmed_regions || [];
|
|
520
|
+
let hasHotspotAnalysis = hotspotAnalysis?.total_builds_analyzed > 0;
|
|
521
|
+
let hasConfirmedRegions = confirmedRegions.length > 0;
|
|
522
|
+
let summary = {
|
|
523
|
+
screenshots: 1,
|
|
524
|
+
hotspots: hasHotspotAnalysis ? 1 : 0,
|
|
525
|
+
regions: hasConfirmedRegions ? 1 : 0,
|
|
526
|
+
total_regions: confirmedRegions.length
|
|
527
|
+
};
|
|
528
|
+
this.saveDynamicMetadataFromBaselineResponse({
|
|
529
|
+
hotspots: hasHotspotAnalysis ? {
|
|
530
|
+
[screenshotName]: hotspotAnalysis
|
|
531
|
+
} : {},
|
|
532
|
+
regions: hasConfirmedRegions ? {
|
|
533
|
+
[screenshotName]: {
|
|
534
|
+
confirmed: confirmedRegions,
|
|
535
|
+
candidates: []
|
|
536
|
+
}
|
|
537
|
+
} : {},
|
|
538
|
+
summary
|
|
539
|
+
});
|
|
540
|
+
}
|
|
522
541
|
|
|
523
542
|
/**
|
|
524
543
|
* Process already-fetched baseline data (for use when caller handles auth)
|
|
@@ -557,20 +576,7 @@ export class TddService {
|
|
|
557
576
|
}
|
|
558
577
|
}
|
|
559
578
|
let baselineBuild = apiResponse.build;
|
|
560
|
-
|
|
561
|
-
// Store bundled hotspots and regions from API response
|
|
562
|
-
let {
|
|
563
|
-
saveHotspotMetadata,
|
|
564
|
-
saveRegionMetadata
|
|
565
|
-
} = this._deps;
|
|
566
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
567
|
-
this.hotspotData = apiResponse.hotspots;
|
|
568
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
569
|
-
}
|
|
570
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
571
|
-
this.regionData = apiResponse.regions;
|
|
572
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
573
|
-
}
|
|
579
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
574
580
|
if (baselineBuild.status === 'failed') {
|
|
575
581
|
output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
|
|
576
582
|
return await this.handleLocalBaselines();
|
|
@@ -915,7 +921,7 @@ export class TddService {
|
|
|
915
921
|
return null;
|
|
916
922
|
}
|
|
917
923
|
this.baselineData = metadata;
|
|
918
|
-
this.threshold = metadata.threshold
|
|
924
|
+
this.threshold = metadata.threshold ?? this.threshold;
|
|
919
925
|
this.signatureProperties = metadata.signatureProperties || this.signatureProperties;
|
|
920
926
|
if (this.signatureProperties.length > 0) {
|
|
921
927
|
output.debug('tdd', `loaded signature properties: ${this.signatureProperties.join(', ')}`);
|
package/dist/types/client.d.ts
CHANGED
|
@@ -4,6 +4,40 @@
|
|
|
4
4
|
* @module @vizzly-testing/cli/client
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export type ClientLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
8
|
+
|
|
9
|
+
export const LOG_LEVELS: Readonly<Record<ClientLogLevel, number>>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check whether client SDK output should log at the requested level.
|
|
13
|
+
*/
|
|
14
|
+
export function shouldLogClient(
|
|
15
|
+
level: string,
|
|
16
|
+
configuredLevel?: string
|
|
17
|
+
): boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auto-discover a local TDD server by searching for `.vizzly/server.json`.
|
|
21
|
+
*/
|
|
22
|
+
export function autoDiscoverTddServer(
|
|
23
|
+
startDir?: string,
|
|
24
|
+
deps?: {
|
|
25
|
+
exists?: (path: string) => boolean;
|
|
26
|
+
readFile?: (path: string, encoding: BufferEncoding) => string | Buffer;
|
|
27
|
+
}
|
|
28
|
+
): string | null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result returned by a successful screenshot capture.
|
|
32
|
+
*/
|
|
33
|
+
export interface ScreenshotResult {
|
|
34
|
+
success: boolean;
|
|
35
|
+
status?: 'passed' | 'failed' | 'new';
|
|
36
|
+
name?: string;
|
|
37
|
+
diffPercentage?: number;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
7
41
|
/**
|
|
8
42
|
* Take a screenshot for visual regression testing
|
|
9
43
|
*
|
|
@@ -38,8 +72,9 @@ export function vizzlyScreenshot(
|
|
|
38
72
|
threshold?: number;
|
|
39
73
|
minClusterSize?: number;
|
|
40
74
|
fullPage?: boolean;
|
|
75
|
+
[key: string]: unknown;
|
|
41
76
|
}
|
|
42
|
-
): Promise<
|
|
77
|
+
): Promise<ScreenshotResult | null>;
|
|
43
78
|
|
|
44
79
|
/**
|
|
45
80
|
* Flush result summary returned by vizzlyFlush
|