@vizzly-testing/cli 0.35.3-beta.2 → 0.35.3-beta.4

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.
Files changed (36) hide show
  1. package/README.md +3 -4
  2. package/dist/api/core.js +12 -5
  3. package/dist/api/endpoints.js +5 -50
  4. package/dist/api/index.js +1 -1
  5. package/dist/cli.js +4 -4
  6. package/dist/client/index.js +2 -1
  7. package/dist/commands/api.js +3 -3
  8. package/dist/commands/builds.js +12 -15
  9. package/dist/commands/comparisons.js +7 -9
  10. package/dist/commands/context.js +3 -24
  11. package/dist/commands/review.js +122 -35
  12. package/dist/commands/run.js +1 -3
  13. package/dist/commands/status.js +35 -79
  14. package/dist/commands/upload.js +1 -1
  15. package/dist/context/local-workspace-provider.js +36 -126
  16. package/dist/reporter/reporter-bundle.iife.js +49 -81
  17. package/dist/screenshot-server/core.js +5 -1
  18. package/dist/sdk/index.js +2 -1
  19. package/dist/server/handlers/api-handler.js +18 -2
  20. package/dist/server/handlers/tdd-handler.js +12 -11
  21. package/dist/server/routers/dashboard.js +5 -1
  22. package/dist/server/routers/screenshot.js +25 -2
  23. package/dist/tdd/runtime-context.js +0 -2
  24. package/dist/tdd/services/comparison-service.js +4 -68
  25. package/dist/tdd/tdd-service.js +10 -208
  26. package/dist/types/index.d.ts +6 -2
  27. package/dist/uploader/core.js +7 -8
  28. package/dist/utils/screenshot-options.js +61 -22
  29. package/dist/utils/visual-context-normalizers.js +86 -139
  30. package/package.json +1 -1
  31. package/skills/vizzly/SKILL.md +4 -4
  32. package/skills/vizzly/references/dynamic-content.md +2 -2
  33. package/dist/tdd/core/hotspot-coverage.js +0 -112
  34. package/dist/tdd/core/region-coverage.js +0 -203
  35. package/dist/tdd/metadata/hotspot-metadata.js +0 -93
  36. package/dist/tdd/metadata/region-metadata.js +0 -56
@@ -4,6 +4,8 @@
4
4
  * No I/O, no side effects - just data transformations.
5
5
  */
6
6
 
7
+ import { getScreenshotOptionsPayload } from '../utils/screenshot-options.js';
8
+
7
9
  // ============================================================================
8
10
  // Request Validation
9
11
  // ============================================================================
@@ -149,9 +151,11 @@ export function buildServerStoppedMessage() {
149
151
  * @returns {{ name: string, image: string, properties?: Object }}
150
152
  */
151
153
  export function extractScreenshotData(body) {
152
- return {
154
+ let screenshotData = {
153
155
  name: body.name,
154
156
  image: body.image,
155
157
  properties: body.properties
156
158
  };
159
+ Object.assign(screenshotData, getScreenshotOptionsPayload(body));
160
+ return screenshotData;
157
161
  }
package/dist/sdk/index.js CHANGED
@@ -19,7 +19,7 @@ import { createUploader } from '../uploader/index.js';
19
19
  import { loadConfig } from '../utils/config-loader.js';
20
20
  import { resolveImageBuffer } from '../utils/file-helpers.js';
21
21
  import * as output from '../utils/output.js';
22
- import { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
22
+ import { getScreenshotOptionsPayload, normalizeScreenshotOptions } from '../utils/screenshot-options.js';
23
23
 
24
24
  /**
25
25
  * Create a new Vizzly instance with custom configuration
@@ -314,6 +314,7 @@ export class VizzlySDK extends EventEmitter {
314
314
  type: 'base64',
315
315
  properties: normalizedOptions.properties
316
316
  };
317
+ Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
317
318
  if (normalizedOptions.warnings.length > 0) {
318
319
  screenshotData.warnings = normalizedOptions.warnings;
319
320
  }
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
4
4
  import { uploadScreenshot as defaultUploadScreenshot } from '../../api/index.js';
5
5
  import { detectImageInputType } from '../../utils/image-input-detector.js';
6
6
  import * as output from '../../utils/output.js';
7
+ import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
7
8
 
8
9
  /**
9
10
  * API Handler - Non-blocking screenshot upload
@@ -47,8 +48,23 @@ export const createApiHandler = (client, {
47
48
  let vizzlyDisabled = false;
48
49
  let screenshotCount = 0;
49
50
  let uploadPromises = [];
50
- const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = []) => {
51
+ const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) => {
51
52
  let handlerStart = Date.now();
53
+ let normalizedOptions = normalizeScreenshotOptions({
54
+ ...screenshotOptions,
55
+ properties
56
+ });
57
+ properties = normalizedOptions.properties;
58
+ screenshotOptions = {
59
+ ...screenshotOptions,
60
+ threshold: normalizedOptions.threshold,
61
+ minClusterSize: normalizedOptions.minClusterSize,
62
+ fullPage: normalizedOptions.fullPage,
63
+ captureMode: normalizedOptions.captureMode,
64
+ deviceScaleFactor: normalizedOptions.deviceScaleFactor,
65
+ selector: normalizedOptions.selector
66
+ };
67
+ warnings = [...(warnings || []), ...normalizedOptions.warnings];
52
68
  output.debug('upload', `${name} received`, {
53
69
  buildId: buildId?.slice(0, 8)
54
70
  });
@@ -131,7 +147,7 @@ export const createApiHandler = (client, {
131
147
  let uploadStart = Date.now();
132
148
 
133
149
  // Fire upload in background - DON'T AWAIT!
134
- let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}).then(result => {
150
+ let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}, false, screenshotOptions).then(result => {
135
151
  let duration = Date.now() - uploadStart;
136
152
  if (!result.skipped) {
137
153
  output.debug('upload', `${name} completed`, {
@@ -5,6 +5,7 @@ import { getDimensionsSync as defaultGetDimensionsSync } from '@vizzly-testing/h
5
5
  import { TddService as DefaultTddService } from '../../tdd/tdd-service.js';
6
6
  import { detectImageInputType as defaultDetectImageInputType } from '../../utils/image-input-detector.js';
7
7
  import * as defaultOutput from '../../utils/output.js';
8
+ import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
8
9
  import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
9
10
 
10
11
  /**
@@ -305,7 +306,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
305
306
  output.debug('tdd', `baseline: ${baseline.buildName}`);
306
307
  }
307
308
  };
308
- const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = []) => {
309
+ const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) => {
309
310
  let handlerStart = Date.now();
310
311
  output.debug('tdd', `${name} received`);
311
312
 
@@ -324,13 +325,18 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
324
325
  };
325
326
  }
326
327
 
327
- // Unwrap double-nested properties if needed (client SDK wraps options in properties field)
328
+ // Preserve the old nested wrapper for older clients, then filter user
329
+ // properties before local comparison/report serialization.
328
330
  let unwrappedProperties = unwrapProperties(properties);
331
+ let normalizedProperties = normalizeScreenshotOptions({
332
+ properties: unwrappedProperties
333
+ });
334
+ warnings = [...(warnings || []), ...normalizedProperties.warnings];
329
335
 
330
336
  // Validate and sanitize properties
331
337
  let validatedProperties;
332
338
  try {
333
- validatedProperties = validateScreenshotProperties(unwrappedProperties);
339
+ validatedProperties = validateScreenshotProperties(normalizedProperties.properties);
334
340
  } catch (error) {
335
341
  return {
336
342
  statusCode: 400,
@@ -419,7 +425,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
419
425
 
420
426
  // Use the sanitized name as-is (no modification with browser/viewport)
421
427
  // Baseline matching uses signature logic (name + viewport_width + browser)
422
- const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties);
428
+ const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties, screenshotOptions);
423
429
 
424
430
  // Comparison tracked by tdd.js event handler
425
431
 
@@ -450,9 +456,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
450
456
  warnings,
451
457
  originalName: name,
452
458
  timestamp: Date.now(),
453
- // Boolean hints so UI can show toggle buttons without fetching heavy data
454
- hasDiffClusters: comparison.diffClusters?.length > 0,
455
- hasConfirmedRegions: comparison.confirmedRegions?.length > 0
459
+ hasDiffClusters: comparison.diffClusters?.length > 0
456
460
  };
457
461
 
458
462
  // Update lightweight comparison in report-data.json (triggers SSE broadcast)
@@ -462,10 +466,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
462
466
  updateComparisonDetails(comparison.id, {
463
467
  diffClusters: comparison.diffClusters,
464
468
  intensityStats: comparison.intensityStats,
465
- boundingBox: comparison.boundingBox,
466
- regionAnalysis: comparison.regionAnalysis,
467
- hotspotAnalysis: comparison.hotspotAnalysis,
468
- confirmedRegions: comparison.confirmedRegions
469
+ boundingBox: comparison.boundingBox
469
470
  });
470
471
 
471
472
  // Log screenshot event for menubar
@@ -10,6 +10,10 @@ import { sendError, sendHtml, sendSuccess } from '../middleware/response.js';
10
10
 
11
11
  // SPA routes that should serve the dashboard HTML
12
12
  const SPA_ROUTES = ['/', '/stats', '/settings', '/projects', '/builds'];
13
+ let COMPARISON_DETAIL_FIELDS = new Set(['boundingBox', 'diffClusters', 'intensityStats']);
14
+ function presentComparisonDetails(details = {}) {
15
+ return Object.fromEntries(Object.entries(details).filter(([field]) => COMPARISON_DETAIL_FIELDS.has(field)));
16
+ }
13
17
 
14
18
  /**
15
19
  * Create dashboard router
@@ -99,7 +103,7 @@ export function createDashboardRouter(context) {
99
103
  if (heavy) {
100
104
  comparison = {
101
105
  ...comparison,
102
- ...heavy
106
+ ...presentComparisonDetails(heavy)
103
107
  };
104
108
  }
105
109
  } catch (error) {
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as output from '../../utils/output.js';
7
+ import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
7
8
  import { parseJsonBody } from '../middleware/json-parser.js';
8
9
  import { sendError, sendJson } from '../middleware/response.js';
9
10
 
@@ -33,7 +34,13 @@ export function createScreenshotRouter({
33
34
  properties,
34
35
  image,
35
36
  type,
36
- warnings
37
+ warnings,
38
+ threshold,
39
+ minClusterSize,
40
+ fullPage,
41
+ captureMode,
42
+ deviceScaleFactor,
43
+ selector
37
44
  } = body;
38
45
  if (!name || !image) {
39
46
  sendError(res, 400, 'name and image are required');
@@ -42,7 +49,23 @@ export function createScreenshotRouter({
42
49
 
43
50
  // Use buildId from request body, or fall back to server's buildId
44
51
  const effectiveBuildId = buildId || defaultBuildId;
45
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type, warnings);
52
+ const normalizedOptions = normalizeScreenshotOptions({
53
+ properties,
54
+ threshold,
55
+ minClusterSize,
56
+ fullPage,
57
+ captureMode,
58
+ deviceScaleFactor,
59
+ selector
60
+ });
61
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, normalizedOptions.properties, type, [...(warnings || []), ...normalizedOptions.warnings], {
62
+ threshold: normalizedOptions.threshold,
63
+ minClusterSize: normalizedOptions.minClusterSize,
64
+ fullPage: normalizedOptions.fullPage,
65
+ captureMode: normalizedOptions.captureMode,
66
+ deviceScaleFactor: normalizedOptions.deviceScaleFactor,
67
+ selector: normalizedOptions.selector
68
+ });
46
69
  sendJson(res, result.statusCode, result.body);
47
70
  return true;
48
71
  } catch (error) {
@@ -9,7 +9,6 @@ export function buildTddDependencyOps(deps = {}, defaults = {}) {
9
9
  colors = defaults.colors,
10
10
  validatePathSecurity = defaults.validatePathSecurity,
11
11
  initializeDirectories = defaults.initializeDirectories,
12
- calculateHotspotCoverage = defaults.calculateHotspotCoverage,
13
12
  fs = {},
14
13
  api = {},
15
14
  metadata = {},
@@ -51,7 +50,6 @@ export function buildTddDependencyOps(deps = {}, defaults = {}) {
51
50
  colors,
52
51
  validatePathSecurity,
53
52
  initializeDirectories,
54
- calculateHotspotCoverage,
55
53
  ...fsOps,
56
54
  ...apiOps,
57
55
  ...metadataOps,
@@ -5,8 +5,6 @@
5
5
  */
6
6
 
7
7
  import { analyze } from '@vizzly-testing/honeydiff';
8
- import { calculateHotspotCoverage } from '../core/hotspot-coverage.js';
9
- import { calculateRegionCoverage, shouldAutoApproveFromRegions } from '../core/region-coverage.js';
10
8
  import { generateComparisonId } from '../core/signature.js';
11
9
 
12
10
  /**
@@ -149,8 +147,6 @@ export function buildNewComparison(params) {
149
147
  * @param {number} params.threshold - Effective threshold used
150
148
  * @param {number} params.minClusterSize - Effective minClusterSize used
151
149
  * @param {Object} params.honeydiffResult - Result from honeydiff
152
- * @param {Object} params.hotspotAnalysis - Hotspot data for this screenshot (optional)
153
- * @param {Object} params.regionData - User-defined region data { confirmed: [], candidates: [] } (optional)
154
150
  * @returns {Object} Comparison result
155
151
  */
156
152
  export function buildFailedComparison(params) {
@@ -163,49 +159,13 @@ export function buildFailedComparison(params) {
163
159
  properties,
164
160
  threshold,
165
161
  minClusterSize,
166
- honeydiffResult,
167
- hotspotAnalysis,
168
- regionData
162
+ honeydiffResult
169
163
  } = params;
170
164
  let diffClusters = honeydiffResult.diffClusters || [];
171
- let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
172
- let isFiltered = false;
173
- let filterReason = 'pixel-diff';
174
-
175
- // Region analysis (user-confirmed 2D boxes) - check FIRST (takes priority)
176
- let regionCoverage = null;
177
- let isRegionFiltered = false;
178
- let confirmedRegions = regionData?.confirmed || [];
179
- if (confirmedRegions.length > 0 && diffClusters.length > 0) {
180
- regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
181
- if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
182
- ssimScore
183
- })) {
184
- isRegionFiltered = true;
185
- isFiltered = true;
186
- filterReason = 'region-filtered';
187
- }
188
- }
189
-
190
- // Hotspot analysis (1D Y-bands from historical data) - check SECOND
191
- let hotspotCoverage = null;
192
- let isHotspotFiltered = false;
193
- if (!isFiltered && hotspotAnalysis && diffClusters.length > 0) {
194
- hotspotCoverage = calculateHotspotCoverage(diffClusters, hotspotAnalysis);
195
-
196
- // Check if diff should be filtered as hotspot noise
197
- // (cloud uses confidence_score >= 70 which is >0.7 when normalized)
198
- let isHighConfidence = hotspotAnalysis.confidence === 'high' || hotspotAnalysis.confidence_score !== undefined && hotspotAnalysis.confidence_score >= 70;
199
- if (isHighConfidence && hotspotCoverage.coverage >= 0.8) {
200
- isHotspotFiltered = true;
201
- isFiltered = true;
202
- filterReason = 'hotspot-filtered';
203
- }
204
- }
205
165
  return {
206
166
  id: generateComparisonId(signature),
207
167
  name,
208
- status: isFiltered ? 'passed' : 'failed',
168
+ status: 'failed',
209
169
  baseline: baselinePath,
210
170
  current: currentPath,
211
171
  diff: diffPath,
@@ -215,38 +175,14 @@ export function buildFailedComparison(params) {
215
175
  minClusterSize,
216
176
  diffPercentage: honeydiffResult.diffPercentage,
217
177
  diffCount: honeydiffResult.diffPixels,
218
- reason: filterReason,
178
+ reason: 'pixel-diff',
219
179
  totalPixels: honeydiffResult.totalPixels,
220
180
  aaPixelsIgnored: honeydiffResult.aaPixelsIgnored,
221
181
  aaPercentage: honeydiffResult.aaPercentage,
222
182
  boundingBox: honeydiffResult.boundingBox,
223
183
  heightDiff: honeydiffResult.heightDiff,
224
184
  intensityStats: honeydiffResult.intensityStats,
225
- diffClusters,
226
- // User-defined region analysis (2D boxes)
227
- regionAnalysis: regionCoverage ? {
228
- coverage: regionCoverage.coverage,
229
- clustersInRegions: regionCoverage.clustersInRegions,
230
- totalClusters: regionCoverage.totalClusters,
231
- pixelsInRegions: regionCoverage.pixelsInRegions,
232
- totalPixels: regionCoverage.totalPixels,
233
- matchedRegions: regionCoverage.matchedRegions,
234
- confirmedCount: confirmedRegions.length,
235
- ssimScore,
236
- isFiltered: isRegionFiltered
237
- } : null,
238
- // Include confirmed regions for visualization in UI
239
- confirmedRegions: confirmedRegions.length > 0 ? confirmedRegions : null,
240
- // Historical hotspot analysis (1D Y-bands)
241
- hotspotAnalysis: hotspotCoverage ? {
242
- coverage: hotspotCoverage.coverage,
243
- linesInHotspots: hotspotCoverage.linesInHotspots,
244
- totalLines: hotspotCoverage.totalLines,
245
- confidence: hotspotAnalysis?.confidence,
246
- confidenceScore: hotspotAnalysis?.confidence_score,
247
- regionCount: hotspotAnalysis?.regions?.length || 0,
248
- isFiltered: isHotspotFiltered
249
- } : null
185
+ diffClusters
250
186
  };
251
187
  }
252
188