@vizzly-testing/cli 0.35.3-beta.0 → 0.35.3-beta.2
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 +15 -7
- package/dist/api/client.js +20 -5
- package/dist/api/core.js +17 -3
- package/dist/cli.js +12 -52
- package/dist/commands/baselines.js +0 -7
- package/dist/commands/config-cmd.js +0 -7
- package/dist/commands/context.js +261 -308
- package/dist/commands/doctor.js +0 -8
- package/dist/commands/init.js +69 -25
- package/dist/commands/login.js +0 -13
- package/dist/commands/logout.js +0 -12
- package/dist/commands/orgs.js +0 -9
- package/dist/commands/whoami.js +0 -12
- package/dist/context/local-workspace-provider.js +276 -30
- package/dist/plugin-loader.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +17 -17
- package/dist/server/routers/events.js +4 -4
- package/dist/tdd/services/comparison-service.js +32 -8
- package/dist/utils/visual-context-normalizers.js +12 -3
- package/package.json +13 -12
- package/skills/vizzly/SKILL.md +42 -52
- package/skills/vizzly/references/cli-context.md +50 -49
- package/skills/vizzly/references/dynamic-content.md +18 -29
- package/skills/vizzly/references/sdks.md +20 -28
- package/skills/vizzly/references/setup-ci.md +18 -42
- package/dist/config/index.js +0 -13
- package/dist/project/index.js +0 -13
- package/dist/tdd/index.js +0 -19
- package/dist/tdd/services/baseline-downloader.js +0 -151
- package/dist/tdd/services/hotspot-service.js +0 -58
- package/dist/utils/environment.js +0 -109
- package/skills/vizzly/agents/openai.yaml +0 -4
package/dist/commands/doctor.js
CHANGED
|
@@ -264,12 +264,4 @@ export async function doctorCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
264
264
|
output.cleanup();
|
|
265
265
|
if (hasErrors) exit(1);
|
|
266
266
|
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Validate doctor options (no specific validation needed)
|
|
271
|
-
* @param {Object} options - Command options
|
|
272
|
-
*/
|
|
273
|
-
export function validateDoctorOptions() {
|
|
274
|
-
return []; // No specific validation for now
|
|
275
267
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -23,6 +23,7 @@ function createInitDeps(deps = {}) {
|
|
|
23
23
|
output: deps.output || output,
|
|
24
24
|
promptAgentSkill: deps.promptAgentSkill || promptAgentSkill,
|
|
25
25
|
readFile: deps.readFile || fs.readFile,
|
|
26
|
+
remove: deps.remove || fs.rm,
|
|
26
27
|
skillSourcePath: deps.skillSourcePath || getPackagedAgentSkillPath(),
|
|
27
28
|
writeFile: deps.writeFile || fs.writeFile
|
|
28
29
|
};
|
|
@@ -37,7 +38,7 @@ function configureOutput(output, options) {
|
|
|
37
38
|
export function getInitConfigPath(cwd = process.cwd()) {
|
|
38
39
|
return path.join(cwd, 'vizzly.config.js');
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
function getPackagedAgentSkillPath(baseDir = commandDir) {
|
|
41
42
|
return path.resolve(baseDir, '..', '..', 'skills', 'vizzly');
|
|
42
43
|
}
|
|
43
44
|
export function getProjectAgentSkillPath(cwd = process.cwd()) {
|
|
@@ -54,10 +55,10 @@ export async function fileExists(filePath, access = fs.access) {
|
|
|
54
55
|
return false;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
function getPluginsWithConfig(plugins = []) {
|
|
58
59
|
return plugins.filter(plugin => plugin.configSchema);
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
function getPluginConfigNames(plugins = []) {
|
|
61
62
|
return getPluginsWithConfig(plugins).map(plugin => plugin.name);
|
|
62
63
|
}
|
|
63
64
|
function formatObjectKey(key) {
|
|
@@ -115,7 +116,7 @@ export function formatPluginConfig(plugin, output = null) {
|
|
|
115
116
|
return '';
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
+
function generatePluginConfigs(plugins = [], output = null) {
|
|
119
120
|
let sections = [];
|
|
120
121
|
for (let plugin of plugins) {
|
|
121
122
|
if (plugin.configSchema) {
|
|
@@ -212,17 +213,21 @@ async function shouldInstallAgentSkill(options, deps) {
|
|
|
212
213
|
}
|
|
213
214
|
let agentGuidanceStart = '<!-- vizzly-agent-guidance -->';
|
|
214
215
|
let agentGuidanceEnd = '<!-- /vizzly-agent-guidance -->';
|
|
215
|
-
|
|
216
|
+
function createAgentGuidanceContent() {
|
|
217
|
+
let skillPath = '.agents/skills/vizzly/SKILL.md';
|
|
216
218
|
return `${agentGuidanceStart}
|
|
217
219
|
## Visual Testing With Vizzly
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
For user-facing changes, use the repo-local Vizzly skill at
|
|
222
|
+
\`${skillPath}\`.
|
|
220
223
|
|
|
221
|
-
-
|
|
222
|
-
-
|
|
223
|
-
-
|
|
224
|
+
- Inspect existing visual evidence before and after the change.
|
|
225
|
+
- Use the repository's established Vizzly command and owning user workflow.
|
|
226
|
+
- Read bounded JSON with \`--agent --json\`, then inspect baseline, current, and
|
|
227
|
+
diff images together.
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
Treat Vizzly diffs as review evidence. Do not approve, reject, or replace
|
|
230
|
+
evidence unless the task explicitly asks for that mutation.
|
|
226
231
|
${agentGuidanceEnd}
|
|
227
232
|
`;
|
|
228
233
|
}
|
|
@@ -232,7 +237,7 @@ function appendSection(content, section) {
|
|
|
232
237
|
}
|
|
233
238
|
return `${content.replace(/\s*$/, '')}\n\n${section}`;
|
|
234
239
|
}
|
|
235
|
-
|
|
240
|
+
async function upsertProjectAgentGuidance({
|
|
236
241
|
cwd,
|
|
237
242
|
access = fs.access,
|
|
238
243
|
readFile = fs.readFile,
|
|
@@ -248,9 +253,28 @@ export async function upsertProjectAgentGuidance({
|
|
|
248
253
|
};
|
|
249
254
|
}
|
|
250
255
|
let existingContent = await readFile(agentsPath, 'utf8');
|
|
251
|
-
|
|
256
|
+
let startIndex = existingContent.indexOf(agentGuidanceStart);
|
|
257
|
+
let endIndex = existingContent.indexOf(agentGuidanceEnd);
|
|
258
|
+
let startCount = existingContent.split(agentGuidanceStart).length - 1;
|
|
259
|
+
let endCount = existingContent.split(agentGuidanceEnd).length - 1;
|
|
260
|
+
if (startCount !== endCount || startCount > 1 || startCount === 1 && endIndex < startIndex) {
|
|
252
261
|
return {
|
|
253
|
-
status: '
|
|
262
|
+
status: 'malformed',
|
|
263
|
+
agentsPath
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (startIndex >= 0 && endIndex >= startIndex) {
|
|
267
|
+
let blockEnd = endIndex + agentGuidanceEnd.length;
|
|
268
|
+
let refreshedContent = `${existingContent.slice(0, startIndex)}${guidance.trimEnd()}${existingContent.slice(blockEnd)}`;
|
|
269
|
+
if (refreshedContent === existingContent) {
|
|
270
|
+
return {
|
|
271
|
+
status: 'exists',
|
|
272
|
+
agentsPath
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
await writeFile(agentsPath, refreshedContent, 'utf8');
|
|
276
|
+
return {
|
|
277
|
+
status: 'refreshed',
|
|
254
278
|
agentsPath
|
|
255
279
|
};
|
|
256
280
|
}
|
|
@@ -263,9 +287,11 @@ export async function upsertProjectAgentGuidance({
|
|
|
263
287
|
export async function installProjectAgentSkill({
|
|
264
288
|
cwd,
|
|
265
289
|
sourcePath,
|
|
290
|
+
refresh = false,
|
|
266
291
|
access = fs.access,
|
|
267
292
|
copy = fs.cp,
|
|
268
|
-
mkdir = fs.mkdir
|
|
293
|
+
mkdir = fs.mkdir,
|
|
294
|
+
remove = fs.rm
|
|
269
295
|
}) {
|
|
270
296
|
let targetPath = getProjectAgentSkillPath(cwd);
|
|
271
297
|
if (!(await fileExists(path.join(sourcePath, 'SKILL.md'), access))) {
|
|
@@ -275,7 +301,8 @@ export async function installProjectAgentSkill({
|
|
|
275
301
|
targetPath
|
|
276
302
|
};
|
|
277
303
|
}
|
|
278
|
-
|
|
304
|
+
let targetExists = await fileExists(targetPath, access);
|
|
305
|
+
if (targetExists && !refresh) {
|
|
279
306
|
return {
|
|
280
307
|
status: 'exists',
|
|
281
308
|
sourcePath,
|
|
@@ -287,11 +314,16 @@ export async function installProjectAgentSkill({
|
|
|
287
314
|
});
|
|
288
315
|
await copy(sourcePath, targetPath, {
|
|
289
316
|
recursive: true,
|
|
290
|
-
errorOnExist:
|
|
291
|
-
force:
|
|
317
|
+
errorOnExist: !targetExists,
|
|
318
|
+
force: targetExists
|
|
292
319
|
});
|
|
320
|
+
if (targetExists) {
|
|
321
|
+
await remove(path.join(targetPath, 'agents', 'openai.yaml'), {
|
|
322
|
+
force: true
|
|
323
|
+
});
|
|
324
|
+
}
|
|
293
325
|
return {
|
|
294
|
-
status: 'installed',
|
|
326
|
+
status: targetExists ? 'refreshed' : 'installed',
|
|
295
327
|
sourcePath,
|
|
296
328
|
targetPath
|
|
297
329
|
};
|
|
@@ -305,6 +337,11 @@ function writeAgentSkillOutput(output, result) {
|
|
|
305
337
|
output.hint(`Installed at ${result.targetPath}`);
|
|
306
338
|
return;
|
|
307
339
|
}
|
|
340
|
+
if (result.status === 'refreshed') {
|
|
341
|
+
output.complete('Refreshed Vizzly agent skill');
|
|
342
|
+
output.hint(`Updated ${result.targetPath}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
308
345
|
if (result.status === 'exists') {
|
|
309
346
|
output.hint('Vizzly agent skill already exists in this repo');
|
|
310
347
|
return;
|
|
@@ -327,8 +364,18 @@ function writeAgentGuidanceOutput(output, result) {
|
|
|
327
364
|
output.hint(`Updated ${result.agentsPath}`);
|
|
328
365
|
return;
|
|
329
366
|
}
|
|
367
|
+
if (result.status === 'refreshed') {
|
|
368
|
+
output.complete('Refreshed Vizzly guidance in AGENTS.md');
|
|
369
|
+
output.hint(`Updated ${result.agentsPath}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
330
372
|
if (result.status === 'exists') {
|
|
331
373
|
output.hint('Vizzly guidance already exists in AGENTS.md');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (result.status === 'malformed') {
|
|
377
|
+
output.warn('Vizzly guidance markers in AGENTS.md are incomplete');
|
|
378
|
+
output.hint(`Repair the managed block in ${result.agentsPath}`);
|
|
332
379
|
}
|
|
333
380
|
}
|
|
334
381
|
function withAgentSetupResults(payload, {
|
|
@@ -362,16 +409,11 @@ async function loadInitPlugins(options, deps) {
|
|
|
362
409
|
}
|
|
363
410
|
try {
|
|
364
411
|
let config = await deps.loadConfig(options.config, {});
|
|
365
|
-
return await deps.loadPlugins(options.config, config
|
|
412
|
+
return await deps.loadPlugins(options.config, config);
|
|
366
413
|
} catch {
|
|
367
414
|
return [];
|
|
368
415
|
}
|
|
369
416
|
}
|
|
370
|
-
|
|
371
|
-
// Export factory function for CLI
|
|
372
|
-
export function createInitCommand(options) {
|
|
373
|
-
return () => init(options);
|
|
374
|
-
}
|
|
375
417
|
export async function init(options = {}, deps = {}) {
|
|
376
418
|
let resolvedDeps = createInitDeps(deps);
|
|
377
419
|
configureOutput(resolvedDeps.output, options);
|
|
@@ -386,7 +428,9 @@ export async function init(options = {}, deps = {}) {
|
|
|
386
428
|
sourcePath: resolvedDeps.skillSourcePath,
|
|
387
429
|
access: resolvedDeps.access,
|
|
388
430
|
copy: resolvedDeps.copy,
|
|
389
|
-
mkdir: resolvedDeps.mkdir
|
|
431
|
+
mkdir: resolvedDeps.mkdir,
|
|
432
|
+
refresh: options.agentGuidance || options.agentSkill,
|
|
433
|
+
remove: resolvedDeps.remove
|
|
390
434
|
});
|
|
391
435
|
}
|
|
392
436
|
if (options.agentGuidance) {
|
package/dist/commands/login.js
CHANGED
|
@@ -236,17 +236,4 @@ export async function loginCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
236
236
|
exit(1);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Validate login options
|
|
243
|
-
* @param {Object} options - Command options
|
|
244
|
-
*/
|
|
245
|
-
export function validateLoginOptions() {
|
|
246
|
-
let errors = [];
|
|
247
|
-
|
|
248
|
-
// No specific validation needed for login command
|
|
249
|
-
// OAuth device flow handles everything via browser
|
|
250
|
-
|
|
251
|
-
return errors;
|
|
252
239
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -70,16 +70,4 @@ export async function logoutCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
70
70
|
output.cleanup();
|
|
71
71
|
exit(1);
|
|
72
72
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Validate logout options
|
|
77
|
-
* @param {Object} options - Command options
|
|
78
|
-
*/
|
|
79
|
-
export function validateLogoutOptions() {
|
|
80
|
-
let errors = [];
|
|
81
|
-
|
|
82
|
-
// No specific validation needed for logout command
|
|
83
|
-
|
|
84
|
-
return errors;
|
|
85
73
|
}
|
package/dist/commands/orgs.js
CHANGED
|
@@ -82,13 +82,4 @@ export async function orgsCommand(_options = {}, globalOptions = {}, deps = {})
|
|
|
82
82
|
output.cleanup();
|
|
83
83
|
exit(1);
|
|
84
84
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate orgs options
|
|
89
|
-
* @param {Object} _options - Command options
|
|
90
|
-
* @returns {string[]} Validation errors
|
|
91
|
-
*/
|
|
92
|
-
export function validateOrgsOptions(_options = {}) {
|
|
93
|
-
return [];
|
|
94
85
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -182,16 +182,4 @@ export async function whoamiCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
182
182
|
exit(1);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Validate whoami options
|
|
189
|
-
* @param {Object} options - Command options
|
|
190
|
-
*/
|
|
191
|
-
export function validateWhoamiOptions() {
|
|
192
|
-
let errors = [];
|
|
193
|
-
|
|
194
|
-
// No specific validation needed for whoami command
|
|
195
|
-
|
|
196
|
-
return errors;
|
|
197
185
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { basename, isAbsolute, join } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
@@ -204,31 +205,6 @@ function buildReviewState(build, reviewSummary) {
|
|
|
204
205
|
unresolved_comments: 0
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
|
-
function mapLocalScreenshot(snapshot, comparison) {
|
|
208
|
-
let mapped = mapLocalComparison(snapshot, comparison);
|
|
209
|
-
let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
|
|
210
|
-
return {
|
|
211
|
-
id: mapped.screenshot.id,
|
|
212
|
-
name: mapped.screenshot.name,
|
|
213
|
-
browser: mapped.screenshot.browser,
|
|
214
|
-
viewport: {
|
|
215
|
-
width: mapped.screenshot.viewport_width,
|
|
216
|
-
height: mapped.screenshot.viewport_height
|
|
217
|
-
},
|
|
218
|
-
url: mapped.screenshot.original_url,
|
|
219
|
-
baseline: mapped.baseline ? {
|
|
220
|
-
id: mapped.baseline.id,
|
|
221
|
-
build_id: baselineBuildId,
|
|
222
|
-
name: mapped.baseline.name,
|
|
223
|
-
browser: mapped.baseline.browser,
|
|
224
|
-
viewport: {
|
|
225
|
-
width: mapped.baseline.viewport_width,
|
|
226
|
-
height: mapped.baseline.viewport_height
|
|
227
|
-
},
|
|
228
|
-
url: mapped.baseline.original_url
|
|
229
|
-
} : null
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
208
|
function mapLocalComparison(snapshot, comparison) {
|
|
233
209
|
let details = snapshot.comparisonDetails[comparison.id] || {};
|
|
234
210
|
let comparisonName = comparison.originalName || comparison.name;
|
|
@@ -306,6 +282,141 @@ function mapLocalComparison(snapshot, comparison) {
|
|
|
306
282
|
}
|
|
307
283
|
};
|
|
308
284
|
}
|
|
285
|
+
function projectLocalScreenshot(screenshot, baseline) {
|
|
286
|
+
if (!screenshot) return null;
|
|
287
|
+
return {
|
|
288
|
+
id: screenshot.id,
|
|
289
|
+
name: screenshot.name,
|
|
290
|
+
browser: screenshot.browser,
|
|
291
|
+
viewport: {
|
|
292
|
+
width: screenshot.viewport_width,
|
|
293
|
+
height: screenshot.viewport_height
|
|
294
|
+
},
|
|
295
|
+
url: screenshot.original_url,
|
|
296
|
+
baseline: baseline ? {
|
|
297
|
+
id: baseline.id,
|
|
298
|
+
build_id: baseline.build_id,
|
|
299
|
+
name: baseline.name,
|
|
300
|
+
browser: baseline.browser,
|
|
301
|
+
viewport: {
|
|
302
|
+
width: baseline.viewport_width,
|
|
303
|
+
height: baseline.viewport_height
|
|
304
|
+
},
|
|
305
|
+
url: baseline.original_url
|
|
306
|
+
} : null
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function projectLocalDiff(diff, includeDiffs) {
|
|
310
|
+
let projected = {
|
|
311
|
+
percentage: diff?.percentage ?? null,
|
|
312
|
+
changed_pixels: diff?.changed_pixels ?? null,
|
|
313
|
+
total_pixels: diff?.total_pixels ?? null,
|
|
314
|
+
threshold: diff?.threshold ?? null,
|
|
315
|
+
image_url: diff?.image_url ?? null,
|
|
316
|
+
fingerprint_hash: diff?.fingerprint_hash ?? null,
|
|
317
|
+
region_count: Array.isArray(diff?.regions) ? diff.regions.length : null
|
|
318
|
+
};
|
|
319
|
+
if (includeDiffs) {
|
|
320
|
+
projected.regions = diff?.regions || [];
|
|
321
|
+
projected.cluster_metadata = diff?.cluster_metadata ?? null;
|
|
322
|
+
projected.fingerprint_data = diff?.fingerprint_data ?? null;
|
|
323
|
+
projected.diff_lines = diff?.diff_lines ?? [];
|
|
324
|
+
}
|
|
325
|
+
return projected;
|
|
326
|
+
}
|
|
327
|
+
function projectLocalEvidence(comparison, includeDiffs) {
|
|
328
|
+
return {
|
|
329
|
+
type: 'comparison',
|
|
330
|
+
id: comparison.id,
|
|
331
|
+
screenshot_name: comparison.screenshot_name,
|
|
332
|
+
status: comparison.status,
|
|
333
|
+
result: comparison.result,
|
|
334
|
+
approval_status: comparison.approval_status,
|
|
335
|
+
needs_review: comparison.needs_review,
|
|
336
|
+
build_id: comparison.build_id,
|
|
337
|
+
build_name: comparison.build_name,
|
|
338
|
+
build_branch: comparison.build_branch,
|
|
339
|
+
build_commit_sha: comparison.build_commit_sha,
|
|
340
|
+
build_created_at: comparison.build_created_at,
|
|
341
|
+
screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
|
|
342
|
+
diff: projectLocalDiff(comparison.diff, includeDiffs)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function projectLocalFocusedComparison(comparison, includeDiffs) {
|
|
346
|
+
if (includeDiffs) return comparison;
|
|
347
|
+
let analysis = comparison.analysis || {};
|
|
348
|
+
let {
|
|
349
|
+
diff_regions: _diffRegions,
|
|
350
|
+
cluster_metadata: _clusterMetadata,
|
|
351
|
+
fingerprint_data: _fingerprintData,
|
|
352
|
+
diff_lines: _diffLines,
|
|
353
|
+
...summaryAnalysis
|
|
354
|
+
} = analysis;
|
|
355
|
+
return {
|
|
356
|
+
...comparison,
|
|
357
|
+
diff: projectLocalDiff(comparison.diff, false),
|
|
358
|
+
analysis: summaryAnalysis
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function projectLocalHistoryItem(comparison) {
|
|
362
|
+
return {
|
|
363
|
+
id: comparison.id,
|
|
364
|
+
screenshot_name: comparison.screenshot_name,
|
|
365
|
+
result: comparison.result,
|
|
366
|
+
status: comparison.status,
|
|
367
|
+
needs_review: comparison.needs_review,
|
|
368
|
+
build_id: comparison.build_id,
|
|
369
|
+
build_name: comparison.build_name,
|
|
370
|
+
build_branch: comparison.build_branch,
|
|
371
|
+
build_created_at: comparison.build_created_at,
|
|
372
|
+
screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
|
|
373
|
+
diff: {
|
|
374
|
+
percentage: comparison.diff?.percentage ?? null,
|
|
375
|
+
fingerprint_hash: comparison.diff?.fingerprint_hash ?? null,
|
|
376
|
+
region_count: Array.isArray(comparison.diff?.regions) ? comparison.diff.regions.length : null
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function compactLocalCollection(items) {
|
|
381
|
+
return {
|
|
382
|
+
total: Array.isArray(items) ? items.length : null,
|
|
383
|
+
included: false,
|
|
384
|
+
details_available: Array.isArray(items) && items.length > 0
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function createLocalDynamicRegions(comparison = null) {
|
|
388
|
+
let confirmedRegions = comparison?.analysis?.confirmed_regions;
|
|
389
|
+
let hotspotAnalysis = comparison?.analysis?.hotspot_analysis;
|
|
390
|
+
return {
|
|
391
|
+
decision: null,
|
|
392
|
+
patterns: compactLocalCollection([]),
|
|
393
|
+
confirmed_regions: compactLocalCollection(confirmedRegions),
|
|
394
|
+
exclusions: {
|
|
395
|
+
total: null,
|
|
396
|
+
included: false,
|
|
397
|
+
details_available: false
|
|
398
|
+
},
|
|
399
|
+
hotspot_analysis: hotspotAnalysis ? {
|
|
400
|
+
total_builds_analyzed: hotspotAnalysis.total_builds_analyzed ?? null,
|
|
401
|
+
confidence: hotspotAnalysis.confidence ?? null,
|
|
402
|
+
confidence_score: hotspotAnalysis.confidence_score ?? null,
|
|
403
|
+
data_source: hotspotAnalysis.data_source ?? null,
|
|
404
|
+
coverage: hotspotAnalysis.coverage ?? null,
|
|
405
|
+
confirmed_region_coverage: hotspotAnalysis.confirmed_region_coverage ?? null
|
|
406
|
+
} : null
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function createLocalSnapshotRevision(snapshot) {
|
|
410
|
+
let revisionInput = {
|
|
411
|
+
serverInfo: snapshot.serverInfo,
|
|
412
|
+
reportData: snapshot.reportData,
|
|
413
|
+
comparisonDetails: snapshot.comparisonDetails,
|
|
414
|
+
baselineMetadata: snapshot.baselineMetadata,
|
|
415
|
+
hotspotFile: snapshot.hotspotFile,
|
|
416
|
+
regionFile: snapshot.regionFile
|
|
417
|
+
};
|
|
418
|
+
return createHash('sha256').update(JSON.stringify(revisionInput)).digest('base64url');
|
|
419
|
+
}
|
|
309
420
|
function buildReviewSummary(comparisons = []) {
|
|
310
421
|
let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
|
|
311
422
|
let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
|
|
@@ -405,17 +516,66 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
405
516
|
} : {})
|
|
406
517
|
};
|
|
407
518
|
}
|
|
408
|
-
function
|
|
519
|
+
function createLocalCursor(resource, target, query, stream, offset, revision) {
|
|
520
|
+
return Buffer.from(JSON.stringify({
|
|
521
|
+
version: 1,
|
|
522
|
+
resource,
|
|
523
|
+
target,
|
|
524
|
+
query,
|
|
525
|
+
stream,
|
|
526
|
+
offset,
|
|
527
|
+
revision
|
|
528
|
+
})).toString('base64url');
|
|
529
|
+
}
|
|
530
|
+
function readLocalCursor(cursor, resource, target, query, revision, streams) {
|
|
531
|
+
if (!cursor) return null;
|
|
532
|
+
try {
|
|
533
|
+
let parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString());
|
|
534
|
+
if (parsed.version !== 1 || parsed.resource !== resource || parsed.target !== target || parsed.query !== query || parsed.revision !== revision || !streams.includes(parsed.stream) || !Number.isInteger(parsed.offset) || parsed.offset < 0) {
|
|
535
|
+
throw new Error('mismatch');
|
|
536
|
+
}
|
|
537
|
+
return parsed;
|
|
538
|
+
} catch {
|
|
539
|
+
throw createLocalWorkspaceError('The local context cursor is invalid or the local results changed. Start again without --cursor.');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function createLocalPage(items, {
|
|
543
|
+
limit,
|
|
544
|
+
offset,
|
|
545
|
+
resource,
|
|
546
|
+
target,
|
|
547
|
+
query,
|
|
548
|
+
stream,
|
|
549
|
+
revision
|
|
550
|
+
}) {
|
|
551
|
+
if (offset > items.length) {
|
|
552
|
+
throw createLocalWorkspaceError('The local context cursor is invalid or the local results changed. Start again without --cursor.');
|
|
553
|
+
}
|
|
554
|
+
let pageItems = items.slice(offset, offset + limit);
|
|
555
|
+
let nextOffset = offset + pageItems.length;
|
|
556
|
+
let hasMore = nextOffset < items.length;
|
|
557
|
+
return {
|
|
558
|
+
items: pageItems,
|
|
559
|
+
page: {
|
|
560
|
+
limit,
|
|
561
|
+
returned: pageItems.length,
|
|
562
|
+
total: items.length,
|
|
563
|
+
has_more: hasMore,
|
|
564
|
+
next_cursor: hasMore ? createLocalCursor(resource, target, query, stream, nextOffset, revision) : null
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function getBuildContext(buildId, query = {}) {
|
|
409
569
|
let snapshot = loadSnapshot();
|
|
410
570
|
let resolvedBuild = buildBuildSnapshot(snapshot);
|
|
411
571
|
if (!(buildId === 'current' || buildId === 'local' || buildId === resolvedBuild.id)) {
|
|
412
572
|
throw createLocalWorkspaceError(`Local workspace context is only available for the active local build (${resolvedBuild.id})`);
|
|
413
573
|
}
|
|
414
574
|
let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
|
|
415
|
-
let mappedScreenshots =
|
|
575
|
+
let mappedScreenshots = mappedComparisons.map(comparison => projectLocalScreenshot(comparison.screenshot, comparison.baseline));
|
|
416
576
|
let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
|
|
417
577
|
let reviewState = buildReviewState(resolvedBuild, reviewSummary);
|
|
418
|
-
|
|
578
|
+
let context = {
|
|
419
579
|
resource: 'build_context',
|
|
420
580
|
source: LOCAL_CONTEXT_SOURCE,
|
|
421
581
|
scope: createScope(),
|
|
@@ -446,8 +606,48 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
446
606
|
},
|
|
447
607
|
links: createBuildLinks(snapshot)
|
|
448
608
|
};
|
|
609
|
+
if (!query.details) return context;
|
|
610
|
+
let revision = createLocalSnapshotRevision(snapshot);
|
|
611
|
+
let limit = query.limit || 10;
|
|
612
|
+
let cursorQuery = `${query.details}:${limit}`;
|
|
613
|
+
let cursor = readLocalCursor(query.cursor, 'build_context', resolvedBuild.id, cursorQuery, revision, ['evidence']);
|
|
614
|
+
let includeDiffs = query.details === 'diffs';
|
|
615
|
+
let evidence = mappedComparisons.map(comparison => projectLocalEvidence(comparison, includeDiffs));
|
|
616
|
+
return {
|
|
617
|
+
resource: 'build_context',
|
|
618
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
619
|
+
scope: context.scope,
|
|
620
|
+
build: context.build,
|
|
621
|
+
baseline: context.baseline,
|
|
622
|
+
status: context.status,
|
|
623
|
+
summary: context.summary,
|
|
624
|
+
preview: null,
|
|
625
|
+
signature_properties: [],
|
|
626
|
+
dynamic_regions: {
|
|
627
|
+
exclusions: {
|
|
628
|
+
total: null,
|
|
629
|
+
included: false,
|
|
630
|
+
details_available: false
|
|
631
|
+
},
|
|
632
|
+
item_details_included: false
|
|
633
|
+
},
|
|
634
|
+
evidence: createLocalPage(evidence, {
|
|
635
|
+
limit,
|
|
636
|
+
offset: cursor?.offset || 0,
|
|
637
|
+
resource: 'build_context',
|
|
638
|
+
target: resolvedBuild.id,
|
|
639
|
+
query: cursorQuery,
|
|
640
|
+
stream: 'evidence',
|
|
641
|
+
revision
|
|
642
|
+
}),
|
|
643
|
+
comments: {
|
|
644
|
+
build: compactLocalCollection(context.comments.build),
|
|
645
|
+
screenshot_count: context.comments.screenshot_count
|
|
646
|
+
},
|
|
647
|
+
links: context.links
|
|
648
|
+
};
|
|
449
649
|
}
|
|
450
|
-
function getComparisonContext(comparisonId) {
|
|
650
|
+
function getComparisonContext(comparisonId, query = {}) {
|
|
451
651
|
let snapshot = loadSnapshot();
|
|
452
652
|
let comparison = findComparison(snapshot, comparisonId);
|
|
453
653
|
if (!comparison) {
|
|
@@ -456,7 +656,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
456
656
|
let mappedComparison = mapLocalComparison(snapshot, comparison);
|
|
457
657
|
let comparisonName = comparison.originalName || comparison.name;
|
|
458
658
|
let history = snapshot.reportData.comparisons.filter(candidate => candidate.id !== comparison.id && (candidate.originalName || candidate.name) === comparisonName).map(candidate => mapLocalComparison(snapshot, candidate));
|
|
459
|
-
|
|
659
|
+
let context = {
|
|
460
660
|
resource: 'comparison_context',
|
|
461
661
|
source: LOCAL_CONTEXT_SOURCE,
|
|
462
662
|
scope: createScope(),
|
|
@@ -485,6 +685,52 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
|
|
|
485
685
|
},
|
|
486
686
|
links: buildComparisonLinks(snapshot, comparison.id)
|
|
487
687
|
};
|
|
688
|
+
if (!query.details) return context;
|
|
689
|
+
let revision = createLocalSnapshotRevision(snapshot);
|
|
690
|
+
let limit = query.limit || 10;
|
|
691
|
+
let cursorQuery = `${query.details}:${limit}`;
|
|
692
|
+
let cursor = readLocalCursor(query.cursor, 'comparison_context', comparison.id, cursorQuery, revision, ['similar_by_fingerprint', 'recent_by_name']);
|
|
693
|
+
let activeStream = cursor?.stream;
|
|
694
|
+
let similarOffset = activeStream === 'similar_by_fingerprint' ? cursor.offset : 0;
|
|
695
|
+
let recentOffset = activeStream === 'recent_by_name' ? cursor.offset : 0;
|
|
696
|
+
return {
|
|
697
|
+
resource: 'comparison_context',
|
|
698
|
+
source: LOCAL_CONTEXT_SOURCE,
|
|
699
|
+
scope: context.scope,
|
|
700
|
+
build: context.build,
|
|
701
|
+
signature_properties: [],
|
|
702
|
+
comparison: projectLocalFocusedComparison(context.comparison, query.details === 'diffs'),
|
|
703
|
+
dynamic_regions: createLocalDynamicRegions(context.comparison),
|
|
704
|
+
history: {
|
|
705
|
+
active_stream: activeStream || null,
|
|
706
|
+
similar_by_fingerprint: createLocalPage([], {
|
|
707
|
+
limit,
|
|
708
|
+
offset: similarOffset,
|
|
709
|
+
resource: 'comparison_context',
|
|
710
|
+
target: comparison.id,
|
|
711
|
+
query: cursorQuery,
|
|
712
|
+
stream: 'similar_by_fingerprint',
|
|
713
|
+
revision
|
|
714
|
+
}),
|
|
715
|
+
recent_by_name: createLocalPage(history.map(projectLocalHistoryItem), {
|
|
716
|
+
limit,
|
|
717
|
+
offset: recentOffset,
|
|
718
|
+
resource: 'comparison_context',
|
|
719
|
+
target: comparison.id,
|
|
720
|
+
query: cursorQuery,
|
|
721
|
+
stream: 'recent_by_name',
|
|
722
|
+
revision
|
|
723
|
+
})
|
|
724
|
+
},
|
|
725
|
+
review: {
|
|
726
|
+
review_summary: context.review.review_summary,
|
|
727
|
+
assignments: compactLocalCollection(context.review.assignments),
|
|
728
|
+
build_comments: compactLocalCollection(context.review.build_comments),
|
|
729
|
+
screenshot_comments: compactLocalCollection(context.review.screenshot_comments)
|
|
730
|
+
},
|
|
731
|
+
links: context.links,
|
|
732
|
+
details: query.details
|
|
733
|
+
};
|
|
488
734
|
}
|
|
489
735
|
function getScreenshotContext(screenshotName) {
|
|
490
736
|
let snapshot = loadSnapshot();
|
package/dist/plugin-loader.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function loadPlugins(configPath, config) {
|
|
|
55
55
|
* Discover installed plugins from node_modules/@vizzly-testing/*
|
|
56
56
|
* @returns {Promise<Array>} Array of plugin info objects
|
|
57
57
|
*/
|
|
58
|
-
|
|
58
|
+
async function discoverInstalledPlugins() {
|
|
59
59
|
let plugins = [];
|
|
60
60
|
try {
|
|
61
61
|
// Find all @vizzly-testing packages
|