@vizzly-testing/cli 0.29.5 → 0.30.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 +28 -0
- 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 +156 -30
- package/dist/commands/context.js +559 -0
- package/dist/commands/logout.js +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/config/core.js +1 -1
- package/dist/context/local-workspace-provider.js +412 -0
- package/dist/context/provider-resolver.js +32 -0
- 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 +1 -1
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/events.js +50 -10
- package/dist/tdd/core/hotspot-coverage.js +1 -1
- package/dist/tdd/services/hotspot-service.js +1 -1
- package/dist/tdd/services/region-service.js +1 -1
- package/dist/types/sdk.d.ts +7 -7
- package/dist/utils/colors.js +46 -44
- package/dist/utils/global-config.js +2 -2
- package/dist/utils/report-data.js +31 -0
- package/dist/utils/security.js +3 -2
- package/package.json +4 -4
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { normalizeReportData } from '../utils/report-data.js';
|
|
4
|
+
let LOCAL_CONTEXT_SOURCE = 'local_workspace';
|
|
5
|
+
let DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT = 50;
|
|
6
|
+
function readJsonIfExists(path) {
|
|
7
|
+
if (!existsSync(path)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function createEmptyReportData() {
|
|
17
|
+
return {
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
comparisons: [],
|
|
20
|
+
summary: {
|
|
21
|
+
total: 0,
|
|
22
|
+
passed: 0,
|
|
23
|
+
failed: 0,
|
|
24
|
+
rejected: 0,
|
|
25
|
+
errors: 0
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function mapComparisonResult(status) {
|
|
30
|
+
if (status === 'new') {
|
|
31
|
+
return 'new';
|
|
32
|
+
}
|
|
33
|
+
if (status === 'failed' || status === 'rejected') {
|
|
34
|
+
return 'changed';
|
|
35
|
+
}
|
|
36
|
+
if (status === 'passed' || status === 'baseline-created') {
|
|
37
|
+
return 'identical';
|
|
38
|
+
}
|
|
39
|
+
if (status === 'error') {
|
|
40
|
+
return 'error';
|
|
41
|
+
}
|
|
42
|
+
return status || 'unknown';
|
|
43
|
+
}
|
|
44
|
+
function mapApprovalStatus(status) {
|
|
45
|
+
if (status === 'failed' || status === 'new') {
|
|
46
|
+
return 'pending';
|
|
47
|
+
}
|
|
48
|
+
if (status === 'rejected') {
|
|
49
|
+
return 'rejected';
|
|
50
|
+
}
|
|
51
|
+
if (status === 'passed' || status === 'baseline-created') {
|
|
52
|
+
return 'approved';
|
|
53
|
+
}
|
|
54
|
+
return status || 'unknown';
|
|
55
|
+
}
|
|
56
|
+
function buildLocalScope(projectRoot) {
|
|
57
|
+
let projectName = basename(projectRoot);
|
|
58
|
+
return {
|
|
59
|
+
organization: {
|
|
60
|
+
id: null,
|
|
61
|
+
name: 'Local Workspace',
|
|
62
|
+
slug: 'local'
|
|
63
|
+
},
|
|
64
|
+
project: {
|
|
65
|
+
id: null,
|
|
66
|
+
name: projectName,
|
|
67
|
+
slug: projectName
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function resolveAssetReference(assetPath, snapshot) {
|
|
72
|
+
if (!assetPath) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (/^https?:\/\//.test(assetPath) || isAbsolute(assetPath)) {
|
|
76
|
+
return assetPath;
|
|
77
|
+
}
|
|
78
|
+
if (assetPath.startsWith('/images/')) {
|
|
79
|
+
if (snapshot.serverInfo?.port) {
|
|
80
|
+
return `http://127.0.0.1:${snapshot.serverInfo.port}${assetPath}`;
|
|
81
|
+
}
|
|
82
|
+
return join(snapshot.vizzlyDir, assetPath.replace('/images/', ''));
|
|
83
|
+
}
|
|
84
|
+
return assetPath;
|
|
85
|
+
}
|
|
86
|
+
function normalizeConfirmedRegions(regions = []) {
|
|
87
|
+
return regions.map((region, index) => ({
|
|
88
|
+
id: region.id || `local-region-${index}`,
|
|
89
|
+
x1: region.x1 ?? region.x ?? null,
|
|
90
|
+
y1: region.y1 ?? region.y ?? null,
|
|
91
|
+
x2: region.x2 ?? (region.x != null && region.width != null ? region.x + region.width : null),
|
|
92
|
+
y2: region.y2 ?? (region.y != null && region.height != null ? region.y + region.height : null),
|
|
93
|
+
label: region.label || null
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
function mergeConfirmedRegions(snapshot, comparisonName, details = {}) {
|
|
97
|
+
let workspaceRegions = snapshot.regions?.[comparisonName]?.confirmed || [];
|
|
98
|
+
let detailRegions = details.confirmedRegions || [];
|
|
99
|
+
let merged = [...workspaceRegions, ...detailRegions];
|
|
100
|
+
let seen = new Set();
|
|
101
|
+
return normalizeConfirmedRegions(merged).filter(region => {
|
|
102
|
+
let key = `${region.label || ''}:${region.x1}:${region.y1}:${region.x2}:${region.y2}`;
|
|
103
|
+
if (seen.has(key)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
seen.add(key);
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function buildHotspotAnalysis(snapshot, comparisonName, details = {}) {
|
|
111
|
+
let hotspotMetadata = snapshot.hotspots?.[comparisonName] || null;
|
|
112
|
+
let hotspotAnalysis = details.hotspotAnalysis || null;
|
|
113
|
+
if (!hotspotMetadata && !hotspotAnalysis) {
|
|
114
|
+
return {
|
|
115
|
+
regions: [],
|
|
116
|
+
total_builds_analyzed: 0,
|
|
117
|
+
confidence: 'no_data',
|
|
118
|
+
confidence_score: null,
|
|
119
|
+
data_source: 'local_workspace'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
regions: hotspotMetadata?.regions || [],
|
|
124
|
+
total_builds_analyzed: 1,
|
|
125
|
+
confidence: hotspotAnalysis?.confidence || hotspotMetadata?.confidence || 'workspace',
|
|
126
|
+
confidence_score: hotspotAnalysis?.confidenceScore || null,
|
|
127
|
+
data_source: 'local_workspace'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function buildComparisonLinks(snapshot, comparisonId) {
|
|
131
|
+
if (!snapshot.serverInfo?.port) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
|
|
136
|
+
comparison_url: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison/${encodeURIComponent(comparisonId)}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function buildBuildSnapshot(snapshot) {
|
|
140
|
+
let buildId = snapshot.session?.buildId || snapshot.serverInfo?.buildId || 'local-workspace';
|
|
141
|
+
return {
|
|
142
|
+
id: buildId,
|
|
143
|
+
name: buildId,
|
|
144
|
+
branch: snapshot.session?.branch || 'local',
|
|
145
|
+
commit_sha: snapshot.session?.commit || null,
|
|
146
|
+
commit_message: null,
|
|
147
|
+
approval_status: snapshot.serverInfo ? 'pending' : 'approved',
|
|
148
|
+
status: snapshot.serverInfo ? 'running' : 'completed',
|
|
149
|
+
created_at: snapshot.session?.createdAt || null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function mapLocalComparison(snapshot, comparison) {
|
|
153
|
+
let details = snapshot.comparisonDetails[comparison.id] || {};
|
|
154
|
+
let comparisonName = comparison.originalName || comparison.name;
|
|
155
|
+
let confirmedRegions = mergeConfirmedRegions(snapshot, comparisonName, details);
|
|
156
|
+
let hotspotAnalysis = buildHotspotAnalysis(snapshot, comparisonName, details);
|
|
157
|
+
let properties = comparison.properties || {};
|
|
158
|
+
let buildSnapshot = buildBuildSnapshot(snapshot);
|
|
159
|
+
return {
|
|
160
|
+
id: comparison.id,
|
|
161
|
+
name: comparisonName,
|
|
162
|
+
status: comparison.status,
|
|
163
|
+
result: mapComparisonResult(comparison.status),
|
|
164
|
+
approval_status: mapApprovalStatus(comparison.status),
|
|
165
|
+
build_id: buildSnapshot.id,
|
|
166
|
+
build_name: buildSnapshot.name,
|
|
167
|
+
build_branch: buildSnapshot.branch,
|
|
168
|
+
build_commit_sha: buildSnapshot.commit_sha,
|
|
169
|
+
build_created_at: buildSnapshot.created_at,
|
|
170
|
+
threshold: comparison.threshold ?? null,
|
|
171
|
+
diff_percentage: comparison.diffPercentage ?? null,
|
|
172
|
+
changed_pixels: comparison.diffCount ?? null,
|
|
173
|
+
total_pixels: comparison.totalPixels ?? null,
|
|
174
|
+
screenshot: {
|
|
175
|
+
id: comparison.id,
|
|
176
|
+
name: comparisonName,
|
|
177
|
+
browser: properties.browser ?? null,
|
|
178
|
+
viewport_width: properties.viewport_width ?? null,
|
|
179
|
+
viewport_height: properties.viewport_height ?? null,
|
|
180
|
+
original_url: resolveAssetReference(comparison.current, snapshot)
|
|
181
|
+
},
|
|
182
|
+
baseline: comparison.baseline ? {
|
|
183
|
+
id: `${comparison.id}-baseline`,
|
|
184
|
+
name: comparisonName,
|
|
185
|
+
browser: properties.browser ?? null,
|
|
186
|
+
viewport_width: properties.viewport_width ?? null,
|
|
187
|
+
viewport_height: properties.viewport_height ?? null,
|
|
188
|
+
original_url: resolveAssetReference(comparison.baseline, snapshot)
|
|
189
|
+
} : null,
|
|
190
|
+
analysis: {
|
|
191
|
+
diff_image_url: resolveAssetReference(comparison.diff, snapshot),
|
|
192
|
+
diff_regions: details.diffClusters || [],
|
|
193
|
+
cluster_metadata: details.diffClusters ? {
|
|
194
|
+
clusterCount: details.diffClusters.length,
|
|
195
|
+
local_workspace: true
|
|
196
|
+
} : null,
|
|
197
|
+
diff_lines: null,
|
|
198
|
+
fingerprint_hash: null,
|
|
199
|
+
fingerprint_data: null,
|
|
200
|
+
hotspot_analysis: hotspotAnalysis,
|
|
201
|
+
region_analysis: details.regionAnalysis || null,
|
|
202
|
+
confirmed_regions: confirmedRegions
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function buildReviewSummary(comparisons = []) {
|
|
207
|
+
let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
|
|
208
|
+
let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
|
|
209
|
+
let pending = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'pending').length;
|
|
210
|
+
return {
|
|
211
|
+
total: comparisons.length,
|
|
212
|
+
pending,
|
|
213
|
+
approved,
|
|
214
|
+
rejected
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function createLocalWorkspaceError(message) {
|
|
218
|
+
let error = new Error(message);
|
|
219
|
+
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
220
|
+
return error;
|
|
221
|
+
}
|
|
222
|
+
export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
223
|
+
let projectRoot = options.projectRoot || process.cwd();
|
|
224
|
+
let readJson = deps.readJsonIfExists || readJsonIfExists;
|
|
225
|
+
let snapshotCache = null;
|
|
226
|
+
function loadSnapshot() {
|
|
227
|
+
if (snapshotCache) {
|
|
228
|
+
return snapshotCache;
|
|
229
|
+
}
|
|
230
|
+
let vizzlyDir = join(projectRoot, '.vizzly');
|
|
231
|
+
snapshotCache = {
|
|
232
|
+
projectRoot,
|
|
233
|
+
vizzlyDir,
|
|
234
|
+
serverInfo: readJson(join(vizzlyDir, 'server.json')),
|
|
235
|
+
session: readJson(join(vizzlyDir, 'session.json')),
|
|
236
|
+
reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
|
|
237
|
+
comparisonDetails: readJson(join(vizzlyDir, 'comparison-details.json')) || {},
|
|
238
|
+
baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json')),
|
|
239
|
+
hotspotFile: readJson(join(vizzlyDir, 'hotspots.json')),
|
|
240
|
+
regionFile: readJson(join(vizzlyDir, 'regions.json'))
|
|
241
|
+
};
|
|
242
|
+
snapshotCache.hotspots = snapshotCache.hotspotFile?.hotspots || null;
|
|
243
|
+
snapshotCache.regions = snapshotCache.regionFile?.regions || null;
|
|
244
|
+
return snapshotCache;
|
|
245
|
+
}
|
|
246
|
+
function isAvailable(snapshot = loadSnapshot()) {
|
|
247
|
+
return Boolean(snapshot.serverInfo || snapshot.session || snapshot.reportData.comparisons.length > 0 || snapshot.baselineMetadata);
|
|
248
|
+
}
|
|
249
|
+
function findComparison(snapshot, target) {
|
|
250
|
+
if (!target) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
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;
|
|
254
|
+
}
|
|
255
|
+
function canHandle(command, target, snapshot = loadSnapshot()) {
|
|
256
|
+
if (!isAvailable(snapshot)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (command === 'build') {
|
|
260
|
+
let buildId = buildBuildSnapshot(snapshot).id;
|
|
261
|
+
return target === 'current' || target === 'local' || target === buildId;
|
|
262
|
+
}
|
|
263
|
+
if (command === 'comparison') {
|
|
264
|
+
return Boolean(findComparison(snapshot, target));
|
|
265
|
+
}
|
|
266
|
+
if (command === 'screenshot') {
|
|
267
|
+
return Boolean(snapshot.reportData.comparisons.some(comparison => (comparison.originalName || comparison.name) === target) || snapshot.regions?.[target] || snapshot.hotspots?.[target]);
|
|
268
|
+
}
|
|
269
|
+
if (command === 'review-queue') {
|
|
270
|
+
return snapshot.reportData.comparisons.length > 0;
|
|
271
|
+
}
|
|
272
|
+
if (command === 'similar') {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
function createScope() {
|
|
278
|
+
return buildLocalScope(projectRoot);
|
|
279
|
+
}
|
|
280
|
+
function createBuildLinks(snapshot) {
|
|
281
|
+
if (!snapshot.serverInfo?.port) {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
let buildId = buildBuildSnapshot(snapshot).id;
|
|
285
|
+
return {
|
|
286
|
+
build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
|
|
287
|
+
comparison_url_prefix: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison`,
|
|
288
|
+
current_build_id: buildId
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function getBuildContext(buildId) {
|
|
292
|
+
let snapshot = loadSnapshot();
|
|
293
|
+
let resolvedBuild = buildBuildSnapshot(snapshot);
|
|
294
|
+
if (!(buildId === 'current' || buildId === 'local' || buildId === resolvedBuild.id)) {
|
|
295
|
+
throw createLocalWorkspaceError(`Local workspace context is only available for the active session build (${resolvedBuild.id})`);
|
|
296
|
+
}
|
|
297
|
+
let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
|
|
298
|
+
let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
|
|
299
|
+
return {
|
|
300
|
+
resource: 'build_context',
|
|
301
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
302
|
+
scope: createScope(),
|
|
303
|
+
build: resolvedBuild,
|
|
304
|
+
summary: {
|
|
305
|
+
comparisons: {
|
|
306
|
+
total: mappedComparisons.length,
|
|
307
|
+
changed: mappedComparisons.filter(comparison => comparison.result === 'changed').length,
|
|
308
|
+
new: mappedComparisons.filter(comparison => comparison.result === 'new').length
|
|
309
|
+
},
|
|
310
|
+
review: reviewSummary
|
|
311
|
+
},
|
|
312
|
+
review: {
|
|
313
|
+
comments: [],
|
|
314
|
+
assignments: []
|
|
315
|
+
},
|
|
316
|
+
comparisons: mappedComparisons,
|
|
317
|
+
links: createBuildLinks(snapshot)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function getComparisonContext(comparisonId) {
|
|
321
|
+
let snapshot = loadSnapshot();
|
|
322
|
+
let comparison = findComparison(snapshot, comparisonId);
|
|
323
|
+
if (!comparison) {
|
|
324
|
+
throw createLocalWorkspaceError(`No local comparison found for "${comparisonId}"`);
|
|
325
|
+
}
|
|
326
|
+
let mappedComparison = mapLocalComparison(snapshot, comparison);
|
|
327
|
+
let comparisonName = comparison.originalName || comparison.name;
|
|
328
|
+
let history = snapshot.reportData.comparisons.filter(candidate => candidate.id !== comparison.id && (candidate.originalName || candidate.name) === comparisonName).map(candidate => mapLocalComparison(snapshot, candidate));
|
|
329
|
+
return {
|
|
330
|
+
resource: 'comparison_context',
|
|
331
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
332
|
+
scope: createScope(),
|
|
333
|
+
build: buildBuildSnapshot(snapshot),
|
|
334
|
+
comparison: mappedComparison,
|
|
335
|
+
history: {
|
|
336
|
+
similar_by_fingerprint: [],
|
|
337
|
+
recent_by_name: history,
|
|
338
|
+
hotspot_analysis: buildHotspotAnalysis(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {}),
|
|
339
|
+
confirmed_regions: mergeConfirmedRegions(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {})
|
|
340
|
+
},
|
|
341
|
+
review: {
|
|
342
|
+
review_summary: {
|
|
343
|
+
total: 0,
|
|
344
|
+
completed: 0,
|
|
345
|
+
pending: 0,
|
|
346
|
+
approved: 0,
|
|
347
|
+
changes_requested: 0,
|
|
348
|
+
commented: 0,
|
|
349
|
+
has_changes_requested: false,
|
|
350
|
+
decisions: []
|
|
351
|
+
},
|
|
352
|
+
assignments: [],
|
|
353
|
+
build_comments: [],
|
|
354
|
+
screenshot_comments: []
|
|
355
|
+
},
|
|
356
|
+
links: buildComparisonLinks(snapshot, comparison.id)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function getScreenshotContext(screenshotName) {
|
|
360
|
+
let snapshot = loadSnapshot();
|
|
361
|
+
let matches = snapshot.reportData.comparisons.filter(comparison => (comparison.originalName || comparison.name) === screenshotName).map(comparison => mapLocalComparison(snapshot, comparison));
|
|
362
|
+
if (matches.length === 0 && !snapshot.regions?.[screenshotName] && !snapshot.hotspots?.[screenshotName]) {
|
|
363
|
+
throw createLocalWorkspaceError(`No local screenshot context found for "${screenshotName}"`);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
resource: 'screenshot_context',
|
|
367
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
368
|
+
scope: createScope(),
|
|
369
|
+
screenshot: {
|
|
370
|
+
name: screenshotName
|
|
371
|
+
},
|
|
372
|
+
hotspot_analysis: buildHotspotAnalysis(snapshot, screenshotName),
|
|
373
|
+
confirmed_regions: mergeConfirmedRegions(snapshot, screenshotName),
|
|
374
|
+
history: {
|
|
375
|
+
recent_comparisons: matches
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function getReviewQueueContext(query = {}) {
|
|
380
|
+
let snapshot = loadSnapshot();
|
|
381
|
+
let unresolved = snapshot.reportData.comparisons.filter(comparison => comparison.status === 'failed' || comparison.status === 'new');
|
|
382
|
+
let offset = query.offset || 0;
|
|
383
|
+
let limit = query.limit || DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT;
|
|
384
|
+
let visible = unresolved.slice(offset, offset + limit).map(comparison => mapLocalComparison(snapshot, comparison));
|
|
385
|
+
return {
|
|
386
|
+
resource: 'review_queue_context',
|
|
387
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
388
|
+
scope: createScope(),
|
|
389
|
+
summary: {
|
|
390
|
+
total: unresolved.length,
|
|
391
|
+
changed: unresolved.filter(comparison => comparison.status === 'failed').length,
|
|
392
|
+
new: unresolved.filter(comparison => comparison.status === 'new').length,
|
|
393
|
+
builds: unresolved.length > 0 ? 1 : 0
|
|
394
|
+
},
|
|
395
|
+
comparisons: visible
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function getSimilarFingerprintContext() {
|
|
399
|
+
throw createLocalWorkspaceError('Local workspace context does not support fingerprint similarity yet. Use --source cloud for this query.');
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
403
|
+
loadSnapshot,
|
|
404
|
+
isAvailable,
|
|
405
|
+
canHandle,
|
|
406
|
+
getBuildContext,
|
|
407
|
+
getComparisonContext,
|
|
408
|
+
getScreenshotContext,
|
|
409
|
+
getReviewQueueContext,
|
|
410
|
+
getSimilarFingerprintContext
|
|
411
|
+
};
|
|
412
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
} = options;
|
|
9
|
+
let {
|
|
10
|
+
createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider
|
|
11
|
+
} = deps;
|
|
12
|
+
if (requestedSource === 'cloud') {
|
|
13
|
+
return 'cloud';
|
|
14
|
+
}
|
|
15
|
+
let localProvider = createLocalWorkspaceContextProvider({
|
|
16
|
+
projectRoot
|
|
17
|
+
});
|
|
18
|
+
let localSnapshot = typeof localProvider.loadSnapshot === 'function' ? localProvider.loadSnapshot() : null;
|
|
19
|
+
let isLocalAvailable = localSnapshot == null ? localProvider.isAvailable() : localProvider.isAvailable(localSnapshot);
|
|
20
|
+
if (requestedSource === 'local') {
|
|
21
|
+
if (!isLocalAvailable) {
|
|
22
|
+
let error = new Error('No local workspace context found. Start a local TDD session or ensure .vizzly/ has report data.');
|
|
23
|
+
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return 'local';
|
|
27
|
+
}
|
|
28
|
+
if (isLocalAvailable && (localSnapshot == null ? localProvider.canHandle(command, target) : localProvider.canHandle(command, target, localSnapshot))) {
|
|
29
|
+
return 'local';
|
|
30
|
+
}
|
|
31
|
+
return 'cloud';
|
|
32
|
+
}
|