@vizzly-testing/cli 0.29.6 → 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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- 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 +870 -0
- 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 +18 -7
- 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 +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- 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 +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- 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/hotspot-coverage.js +1 -1
- 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 +1 -14
- 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 +47 -7
- 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/colors.js +46 -44
- 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 +22 -3
- 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/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- 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
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { normalizeReportData } from '../utils/report-data.js';
|
|
5
|
+
let LOCAL_CONTEXT_SOURCE = 'local_workspace';
|
|
6
|
+
let DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT = 50;
|
|
7
|
+
function readJsonIfExists(path) {
|
|
8
|
+
if (!defaultExistsSync(path)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function createEmptyReportData() {
|
|
18
|
+
return {
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
comparisons: [],
|
|
21
|
+
summary: {
|
|
22
|
+
total: 0,
|
|
23
|
+
passed: 0,
|
|
24
|
+
failed: 0,
|
|
25
|
+
rejected: 0,
|
|
26
|
+
errors: 0
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function mapComparisonResult(status) {
|
|
31
|
+
if (status === 'new') {
|
|
32
|
+
return 'new';
|
|
33
|
+
}
|
|
34
|
+
if (status === 'failed' || status === 'rejected') {
|
|
35
|
+
return 'changed';
|
|
36
|
+
}
|
|
37
|
+
if (status === 'passed' || status === 'baseline-created') {
|
|
38
|
+
return 'identical';
|
|
39
|
+
}
|
|
40
|
+
if (status === 'error') {
|
|
41
|
+
return 'error';
|
|
42
|
+
}
|
|
43
|
+
return status || 'unknown';
|
|
44
|
+
}
|
|
45
|
+
function mapApprovalStatus(status) {
|
|
46
|
+
if (status === 'failed' || status === 'new') {
|
|
47
|
+
return 'pending';
|
|
48
|
+
}
|
|
49
|
+
if (status === 'rejected') {
|
|
50
|
+
return 'rejected';
|
|
51
|
+
}
|
|
52
|
+
if (status === 'passed' || status === 'baseline-created') {
|
|
53
|
+
return 'approved';
|
|
54
|
+
}
|
|
55
|
+
return status || 'unknown';
|
|
56
|
+
}
|
|
57
|
+
function buildLocalScope(projectRoot) {
|
|
58
|
+
let projectName = basename(projectRoot);
|
|
59
|
+
return {
|
|
60
|
+
organization: {
|
|
61
|
+
id: null,
|
|
62
|
+
name: 'Local Workspace',
|
|
63
|
+
slug: 'local'
|
|
64
|
+
},
|
|
65
|
+
project: {
|
|
66
|
+
id: null,
|
|
67
|
+
name: projectName,
|
|
68
|
+
slug: projectName
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function resolveAssetReference(assetPath, snapshot) {
|
|
73
|
+
if (!assetPath) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (/^https?:\/\//.test(assetPath)) {
|
|
77
|
+
return assetPath;
|
|
78
|
+
}
|
|
79
|
+
if (assetPath.startsWith('/images/')) {
|
|
80
|
+
if (snapshot.serverInfo?.port) {
|
|
81
|
+
return `http://127.0.0.1:${snapshot.serverInfo.port}${assetPath}`;
|
|
82
|
+
}
|
|
83
|
+
return join(snapshot.vizzlyDir, assetPath.replace('/images/', ''));
|
|
84
|
+
}
|
|
85
|
+
if (isAbsolute(assetPath)) {
|
|
86
|
+
return assetPath;
|
|
87
|
+
}
|
|
88
|
+
return assetPath;
|
|
89
|
+
}
|
|
90
|
+
function normalizeConfirmedRegions(regions = []) {
|
|
91
|
+
return regions.map((region, index) => ({
|
|
92
|
+
id: region.id || `local-region-${index}`,
|
|
93
|
+
x1: region.x1 ?? region.x ?? null,
|
|
94
|
+
y1: region.y1 ?? region.y ?? null,
|
|
95
|
+
x2: region.x2 ?? (region.x != null && region.width != null ? region.x + region.width : null),
|
|
96
|
+
y2: region.y2 ?? (region.y != null && region.height != null ? region.y + region.height : null),
|
|
97
|
+
label: region.label || null
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
function mergeConfirmedRegions(snapshot, comparisonName, details = {}) {
|
|
101
|
+
let workspaceRegions = snapshot.regions?.[comparisonName]?.confirmed || [];
|
|
102
|
+
let detailRegions = details.confirmedRegions || [];
|
|
103
|
+
let merged = [...workspaceRegions, ...detailRegions];
|
|
104
|
+
let seen = new Set();
|
|
105
|
+
return normalizeConfirmedRegions(merged).filter(region => {
|
|
106
|
+
let key = `${region.label || ''}:${region.x1}:${region.y1}:${region.x2}:${region.y2}`;
|
|
107
|
+
if (seen.has(key)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
seen.add(key);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function buildHotspotAnalysis(snapshot, comparisonName, details = {}) {
|
|
115
|
+
let hotspotMetadata = snapshot.hotspots?.[comparisonName] || null;
|
|
116
|
+
let hotspotAnalysis = details.hotspotAnalysis || null;
|
|
117
|
+
if (!hotspotMetadata && !hotspotAnalysis) {
|
|
118
|
+
return {
|
|
119
|
+
regions: [],
|
|
120
|
+
total_builds_analyzed: 0,
|
|
121
|
+
confidence: 'no_data',
|
|
122
|
+
confidence_score: null,
|
|
123
|
+
data_source: 'local_workspace'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
regions: hotspotMetadata?.regions || [],
|
|
128
|
+
total_builds_analyzed: 1,
|
|
129
|
+
confidence: hotspotAnalysis?.confidence || hotspotMetadata?.confidence || 'workspace',
|
|
130
|
+
confidence_score: hotspotAnalysis?.confidenceScore || null,
|
|
131
|
+
data_source: 'local_workspace'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function buildComparisonLinks(snapshot, comparisonId) {
|
|
135
|
+
if (!snapshot.serverInfo?.port) {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
|
|
140
|
+
comparison_url: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison/${encodeURIComponent(comparisonId)}`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function buildBuildSnapshot(snapshot) {
|
|
144
|
+
let buildId = snapshot.session?.buildId || snapshot.serverInfo?.buildId || 'local-workspace';
|
|
145
|
+
return {
|
|
146
|
+
id: buildId,
|
|
147
|
+
name: buildId,
|
|
148
|
+
branch: snapshot.session?.branch || 'local',
|
|
149
|
+
commit_sha: snapshot.session?.commit || null,
|
|
150
|
+
commit_message: null,
|
|
151
|
+
approval_status: snapshot.serverInfo ? 'pending' : 'approved',
|
|
152
|
+
status: snapshot.serverInfo ? 'running' : 'completed',
|
|
153
|
+
created_at: snapshot.session?.createdAt || null
|
|
154
|
+
};
|
|
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
|
+
}
|
|
221
|
+
function mapLocalComparison(snapshot, comparison) {
|
|
222
|
+
let details = snapshot.comparisonDetails[comparison.id] || {};
|
|
223
|
+
let comparisonName = comparison.originalName || comparison.name;
|
|
224
|
+
let confirmedRegions = mergeConfirmedRegions(snapshot, comparisonName, details);
|
|
225
|
+
let hotspotAnalysis = buildHotspotAnalysis(snapshot, comparisonName, details);
|
|
226
|
+
let properties = comparison.properties || {};
|
|
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 || [];
|
|
233
|
+
return {
|
|
234
|
+
id: comparison.id,
|
|
235
|
+
name: comparisonName,
|
|
236
|
+
screenshot_name: comparisonName,
|
|
237
|
+
status: comparison.status,
|
|
238
|
+
result,
|
|
239
|
+
approval_status: approvalStatus,
|
|
240
|
+
needs_review: approvalStatus === 'pending' && ['changed', 'new'].includes(result),
|
|
241
|
+
build_id: buildSnapshot.id,
|
|
242
|
+
build_name: buildSnapshot.name,
|
|
243
|
+
build_branch: buildSnapshot.branch,
|
|
244
|
+
build_commit_sha: buildSnapshot.commit_sha,
|
|
245
|
+
build_created_at: buildSnapshot.created_at,
|
|
246
|
+
threshold: comparison.threshold ?? null,
|
|
247
|
+
diff_percentage: comparison.diffPercentage ?? null,
|
|
248
|
+
changed_pixels: comparison.diffCount ?? null,
|
|
249
|
+
total_pixels: comparison.totalPixels ?? null,
|
|
250
|
+
screenshot: {
|
|
251
|
+
id: comparison.id,
|
|
252
|
+
name: comparisonName,
|
|
253
|
+
browser: properties.browser ?? null,
|
|
254
|
+
viewport_width: properties.viewport_width ?? null,
|
|
255
|
+
viewport_height: properties.viewport_height ?? null,
|
|
256
|
+
original_url: resolveAssetReference(comparison.current, snapshot)
|
|
257
|
+
},
|
|
258
|
+
baseline: comparison.baseline ? {
|
|
259
|
+
id: `${comparison.id}-baseline`,
|
|
260
|
+
build_id: baselineBuildId,
|
|
261
|
+
name: comparisonName,
|
|
262
|
+
browser: properties.browser ?? null,
|
|
263
|
+
viewport_width: properties.viewport_width ?? null,
|
|
264
|
+
viewport_height: properties.viewport_height ?? null,
|
|
265
|
+
original_url: resolveAssetReference(comparison.baseline, snapshot)
|
|
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
|
+
},
|
|
282
|
+
analysis: {
|
|
283
|
+
diff_image_url: diffImageUrl,
|
|
284
|
+
diff_regions: diffRegions,
|
|
285
|
+
cluster_metadata: diffRegions.length ? {
|
|
286
|
+
clusterCount: diffRegions.length,
|
|
287
|
+
local_workspace: true
|
|
288
|
+
} : null,
|
|
289
|
+
diff_lines: null,
|
|
290
|
+
fingerprint_hash: null,
|
|
291
|
+
fingerprint_data: null,
|
|
292
|
+
hotspot_analysis: hotspotAnalysis,
|
|
293
|
+
region_analysis: details.regionAnalysis || null,
|
|
294
|
+
confirmed_regions: confirmedRegions
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function buildReviewSummary(comparisons = []) {
|
|
299
|
+
let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
|
|
300
|
+
let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
|
|
301
|
+
let pending = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'pending').length;
|
|
302
|
+
return {
|
|
303
|
+
total: comparisons.length,
|
|
304
|
+
pending,
|
|
305
|
+
approved,
|
|
306
|
+
rejected
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function createLocalWorkspaceError(message) {
|
|
310
|
+
let error = new Error(message);
|
|
311
|
+
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
312
|
+
return error;
|
|
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
|
+
}
|
|
321
|
+
export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
322
|
+
let projectRoot = options.projectRoot || process.cwd();
|
|
323
|
+
let readJson = deps.readJsonIfExists || readJsonIfExists;
|
|
324
|
+
let existsSync = deps.existsSync || defaultExistsSync;
|
|
325
|
+
let snapshotCache = null;
|
|
326
|
+
function loadSnapshot() {
|
|
327
|
+
if (snapshotCache) {
|
|
328
|
+
return snapshotCache;
|
|
329
|
+
}
|
|
330
|
+
let vizzlyDir = join(projectRoot, '.vizzly');
|
|
331
|
+
snapshotCache = {
|
|
332
|
+
projectRoot,
|
|
333
|
+
vizzlyDir,
|
|
334
|
+
existsSync,
|
|
335
|
+
serverInfo: readJson(join(vizzlyDir, 'server.json')),
|
|
336
|
+
session: readJson(join(vizzlyDir, 'session.json')),
|
|
337
|
+
reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
|
|
338
|
+
comparisonDetails: readJson(join(vizzlyDir, 'comparison-details.json')) || {},
|
|
339
|
+
baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json')),
|
|
340
|
+
hotspotFile: readJson(join(vizzlyDir, 'hotspots.json')),
|
|
341
|
+
regionFile: readJson(join(vizzlyDir, 'regions.json'))
|
|
342
|
+
};
|
|
343
|
+
snapshotCache.hotspots = snapshotCache.hotspotFile?.hotspots || null;
|
|
344
|
+
snapshotCache.regions = snapshotCache.regionFile?.regions || null;
|
|
345
|
+
return snapshotCache;
|
|
346
|
+
}
|
|
347
|
+
function isAvailable(snapshot = loadSnapshot()) {
|
|
348
|
+
return Boolean(snapshot.serverInfo || snapshot.session || snapshot.reportData.comparisons.length > 0 || snapshot.baselineMetadata);
|
|
349
|
+
}
|
|
350
|
+
function findComparison(snapshot, target) {
|
|
351
|
+
if (!target) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return snapshot.reportData.comparisons.find(comparison => comparison.id === target) || snapshot.reportData.comparisons.find(comparison => comparison.signature === target) || snapshot.reportData.comparisons.find(comparison => (comparison.originalName || comparison.name) === target) || null;
|
|
355
|
+
}
|
|
356
|
+
function canHandle(command, target, snapshot = loadSnapshot()) {
|
|
357
|
+
if (!isAvailable(snapshot)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (command === 'build') {
|
|
361
|
+
let buildId = buildBuildSnapshot(snapshot).id;
|
|
362
|
+
return target === 'current' || target === 'local' || target === buildId;
|
|
363
|
+
}
|
|
364
|
+
if (command === 'comparison') {
|
|
365
|
+
return Boolean(findComparison(snapshot, target));
|
|
366
|
+
}
|
|
367
|
+
if (command === 'screenshot') {
|
|
368
|
+
return Boolean(snapshot.reportData.comparisons.some(comparison => (comparison.originalName || comparison.name) === target) || snapshot.regions?.[target] || snapshot.hotspots?.[target]);
|
|
369
|
+
}
|
|
370
|
+
if (command === 'review-queue') {
|
|
371
|
+
return snapshot.reportData.comparisons.length > 0;
|
|
372
|
+
}
|
|
373
|
+
if (command === 'similar') {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
function createScope() {
|
|
379
|
+
return buildLocalScope(projectRoot);
|
|
380
|
+
}
|
|
381
|
+
function createBuildLinks(snapshot) {
|
|
382
|
+
let reportUrl = createReportUrl(snapshot);
|
|
383
|
+
if (!snapshot.serverInfo?.port) {
|
|
384
|
+
return reportUrl ? {
|
|
385
|
+
report_url: reportUrl
|
|
386
|
+
} : {};
|
|
387
|
+
}
|
|
388
|
+
let buildId = buildBuildSnapshot(snapshot).id;
|
|
389
|
+
return {
|
|
390
|
+
build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
|
|
391
|
+
comparison_url_prefix: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison`,
|
|
392
|
+
current_build_id: buildId,
|
|
393
|
+
...(reportUrl ? {
|
|
394
|
+
report_url: reportUrl
|
|
395
|
+
} : {})
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function getBuildContext(buildId) {
|
|
399
|
+
let snapshot = loadSnapshot();
|
|
400
|
+
let resolvedBuild = buildBuildSnapshot(snapshot);
|
|
401
|
+
if (!(buildId === 'current' || buildId === 'local' || buildId === resolvedBuild.id)) {
|
|
402
|
+
throw createLocalWorkspaceError(`Local workspace context is only available for the active session build (${resolvedBuild.id})`);
|
|
403
|
+
}
|
|
404
|
+
let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
|
|
405
|
+
let mappedScreenshots = snapshot.reportData.comparisons.map(comparison => mapLocalScreenshot(snapshot, comparison));
|
|
406
|
+
let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
|
|
407
|
+
let reviewState = buildReviewState(resolvedBuild, reviewSummary);
|
|
408
|
+
return {
|
|
409
|
+
resource: 'build_context',
|
|
410
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
411
|
+
scope: createScope(),
|
|
412
|
+
build: resolvedBuild,
|
|
413
|
+
baseline: buildBaselineSnapshot(snapshot),
|
|
414
|
+
status: reviewState,
|
|
415
|
+
summary: {
|
|
416
|
+
comparisons: {
|
|
417
|
+
total: mappedComparisons.length,
|
|
418
|
+
changed: mappedComparisons.filter(comparison => comparison.result === 'changed').length,
|
|
419
|
+
new: mappedComparisons.filter(comparison => comparison.result === 'new').length
|
|
420
|
+
},
|
|
421
|
+
review: reviewSummary,
|
|
422
|
+
comments: {
|
|
423
|
+
build: 0,
|
|
424
|
+
screenshot: 0
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
review: {
|
|
428
|
+
comments: [],
|
|
429
|
+
assignments: []
|
|
430
|
+
},
|
|
431
|
+
screenshots: mappedScreenshots,
|
|
432
|
+
comparisons: mappedComparisons,
|
|
433
|
+
comments: {
|
|
434
|
+
build: [],
|
|
435
|
+
screenshot_count: 0
|
|
436
|
+
},
|
|
437
|
+
links: createBuildLinks(snapshot)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function getComparisonContext(comparisonId) {
|
|
441
|
+
let snapshot = loadSnapshot();
|
|
442
|
+
let comparison = findComparison(snapshot, comparisonId);
|
|
443
|
+
if (!comparison) {
|
|
444
|
+
throw createLocalWorkspaceError(`No local comparison found for "${comparisonId}"`);
|
|
445
|
+
}
|
|
446
|
+
let mappedComparison = mapLocalComparison(snapshot, comparison);
|
|
447
|
+
let comparisonName = comparison.originalName || comparison.name;
|
|
448
|
+
let history = snapshot.reportData.comparisons.filter(candidate => candidate.id !== comparison.id && (candidate.originalName || candidate.name) === comparisonName).map(candidate => mapLocalComparison(snapshot, candidate));
|
|
449
|
+
return {
|
|
450
|
+
resource: 'comparison_context',
|
|
451
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
452
|
+
scope: createScope(),
|
|
453
|
+
build: buildBuildSnapshot(snapshot),
|
|
454
|
+
comparison: mappedComparison,
|
|
455
|
+
history: {
|
|
456
|
+
similar_by_fingerprint: [],
|
|
457
|
+
recent_by_name: history,
|
|
458
|
+
hotspot_analysis: buildHotspotAnalysis(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {}),
|
|
459
|
+
confirmed_regions: mergeConfirmedRegions(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {})
|
|
460
|
+
},
|
|
461
|
+
review: {
|
|
462
|
+
review_summary: {
|
|
463
|
+
total: 0,
|
|
464
|
+
completed: 0,
|
|
465
|
+
pending: 0,
|
|
466
|
+
approved: 0,
|
|
467
|
+
changes_requested: 0,
|
|
468
|
+
commented: 0,
|
|
469
|
+
has_changes_requested: false,
|
|
470
|
+
decisions: []
|
|
471
|
+
},
|
|
472
|
+
assignments: [],
|
|
473
|
+
build_comments: [],
|
|
474
|
+
screenshot_comments: []
|
|
475
|
+
},
|
|
476
|
+
links: buildComparisonLinks(snapshot, comparison.id)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function getScreenshotContext(screenshotName) {
|
|
480
|
+
let snapshot = loadSnapshot();
|
|
481
|
+
let matches = snapshot.reportData.comparisons.filter(comparison => (comparison.originalName || comparison.name) === screenshotName).map(comparison => mapLocalComparison(snapshot, comparison));
|
|
482
|
+
if (matches.length === 0 && !snapshot.regions?.[screenshotName] && !snapshot.hotspots?.[screenshotName]) {
|
|
483
|
+
throw createLocalWorkspaceError(`No local screenshot context found for "${screenshotName}"`);
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
resource: 'screenshot_context',
|
|
487
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
488
|
+
scope: createScope(),
|
|
489
|
+
screenshot: {
|
|
490
|
+
name: screenshotName
|
|
491
|
+
},
|
|
492
|
+
hotspot_analysis: buildHotspotAnalysis(snapshot, screenshotName),
|
|
493
|
+
confirmed_regions: mergeConfirmedRegions(snapshot, screenshotName),
|
|
494
|
+
history: {
|
|
495
|
+
recent_comparisons: matches
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function getReviewQueueContext(query = {}) {
|
|
500
|
+
let snapshot = loadSnapshot();
|
|
501
|
+
let unresolved = snapshot.reportData.comparisons.filter(comparison => comparison.status === 'failed' || comparison.status === 'new');
|
|
502
|
+
let offset = query.offset || 0;
|
|
503
|
+
let limit = query.limit || DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT;
|
|
504
|
+
let visible = unresolved.slice(offset, offset + limit).map(comparison => mapLocalComparison(snapshot, comparison));
|
|
505
|
+
return {
|
|
506
|
+
resource: 'review_queue_context',
|
|
507
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
508
|
+
scope: createScope(),
|
|
509
|
+
summary: {
|
|
510
|
+
total: unresolved.length,
|
|
511
|
+
changed: unresolved.filter(comparison => comparison.status === 'failed').length,
|
|
512
|
+
new: unresolved.filter(comparison => comparison.status === 'new').length,
|
|
513
|
+
builds: unresolved.length > 0 ? 1 : 0
|
|
514
|
+
},
|
|
515
|
+
comparisons: visible
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function getSimilarFingerprintContext() {
|
|
519
|
+
throw createLocalWorkspaceError('Local workspace context does not support fingerprint similarity yet. Use --source cloud for this query.');
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
523
|
+
loadSnapshot,
|
|
524
|
+
isAvailable,
|
|
525
|
+
canHandle,
|
|
526
|
+
getBuildContext,
|
|
527
|
+
getComparisonContext,
|
|
528
|
+
getScreenshotContext,
|
|
529
|
+
getReviewQueueContext,
|
|
530
|
+
getSimilarFingerprintContext
|
|
531
|
+
};
|
|
532
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createLocalWorkspaceContextProvider as defaultCreateLocalWorkspaceContextProvider } from './local-workspace-provider.js';
|
|
2
|
+
export function resolveContextSource(options = {}, deps = {}) {
|
|
3
|
+
let {
|
|
4
|
+
requestedSource = 'auto',
|
|
5
|
+
command,
|
|
6
|
+
target = null,
|
|
7
|
+
projectRoot = process.cwd(),
|
|
8
|
+
hasCloudScope = false
|
|
9
|
+
} = options;
|
|
10
|
+
let {
|
|
11
|
+
createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider
|
|
12
|
+
} = deps;
|
|
13
|
+
if (requestedSource === 'cloud') {
|
|
14
|
+
return 'cloud';
|
|
15
|
+
}
|
|
16
|
+
let localProvider = createLocalWorkspaceContextProvider({
|
|
17
|
+
projectRoot
|
|
18
|
+
});
|
|
19
|
+
let localSnapshot = typeof localProvider.loadSnapshot === 'function' ? localProvider.loadSnapshot() : null;
|
|
20
|
+
let isLocalAvailable = localSnapshot == null ? localProvider.isAvailable() : localProvider.isAvailable(localSnapshot);
|
|
21
|
+
if (requestedSource === 'local') {
|
|
22
|
+
if (!isLocalAvailable) {
|
|
23
|
+
let error = new Error('No local workspace context found. Start a local TDD session or ensure .vizzly/ has report data.');
|
|
24
|
+
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return 'local';
|
|
28
|
+
}
|
|
29
|
+
if (hasCloudScope) {
|
|
30
|
+
return 'cloud';
|
|
31
|
+
}
|
|
32
|
+
if (isLocalAvailable && (localSnapshot == null ? localProvider.canHandle(command, target) : localProvider.canHandle(command, target, localSnapshot))) {
|
|
33
|
+
return 'local';
|
|
34
|
+
}
|
|
35
|
+
return 'cloud';
|
|
36
|
+
}
|
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
|