@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
@@ -9,19 +9,16 @@
9
9
  */
10
10
 
11
11
  import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
12
- import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getComparisonContext as defaultGetComparisonContext, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
12
+ import { createApiClient as defaultCreateApiClient, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
13
13
  import { NetworkError } from '../errors/vizzly-error.js';
14
14
  import { colors as defaultColors } from '../utils/colors.js';
15
15
  import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
16
16
  import { getDefaultBranch as defaultGetDefaultBranch } from '../utils/git.js';
17
17
  import * as defaultOutput from '../utils/output.js';
18
18
  import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validatePathSecurity as defaultValidatePathSecurity, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../utils/security.js';
19
- import { calculateHotspotCoverage as defaultCalculateHotspotCoverage } from './core/hotspot-coverage.js';
20
19
  // Import from extracted modules
21
20
  import { generateBaselineFilename as defaultGenerateBaselineFilename, generateComparisonId as defaultGenerateComparisonId, generateScreenshotSignature as defaultGenerateScreenshotSignature } from './core/signature.js';
22
21
  import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, loadBaselineMetadata as defaultLoadBaselineMetadata, saveBaselineMetadata as defaultSaveBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from './metadata/baseline-metadata.js';
23
- import { loadHotspotMetadata as defaultLoadHotspotMetadata, saveHotspotMetadata as defaultSaveHotspotMetadata } from './metadata/hotspot-metadata.js';
24
- import { loadRegionMetadata as defaultLoadRegionMetadata, saveRegionMetadata as defaultSaveRegionMetadata } from './metadata/region-metadata.js';
25
22
  import { buildTddDependencyOps, resolveTddPaths, resolveTddWorkingDirectory } from './runtime-context.js';
26
23
  import { baselineExists as defaultBaselineExists, clearBaselineData as defaultClearBaselineData, clearRunData as defaultClearRunData, getBaselinePath as defaultGetBaselinePath, getCurrentPath as defaultGetCurrentPath, getDiffPath as defaultGetDiffPath, initializeDirectories as defaultInitializeDirectories, saveBaseline as defaultSaveBaseline, saveCurrent as defaultSaveCurrent } from './services/baseline-manager.js';
27
24
  import { buildErrorComparison as defaultBuildErrorComparison, buildFailedComparison as defaultBuildFailedComparison, buildNewComparison as defaultBuildNewComparison, buildPassedComparison as defaultBuildPassedComparison, compareImages as defaultCompareImages, isDimensionMismatchError as defaultIsDimensionMismatchError } from './services/comparison-service.js';
@@ -46,7 +43,6 @@ export class TddService {
46
43
  colors: defaultColors,
47
44
  validatePathSecurity: defaultValidatePathSecurity,
48
45
  initializeDirectories: defaultInitializeDirectories,
49
- calculateHotspotCoverage: defaultCalculateHotspotCoverage,
50
46
  fs: {
51
47
  existsSync: defaultExistsSync,
52
48
  mkdirSync: defaultMkdirSync,
@@ -58,8 +54,6 @@ export class TddService {
58
54
  getTddBaselines: defaultGetTddBaselines,
59
55
  getBuilds: defaultGetBuilds,
60
56
  getComparison: defaultGetComparison,
61
- getComparisonContext: defaultGetComparisonContext,
62
- getBatchHotspots: defaultGetBatchHotspots,
63
57
  fetchWithTimeout: defaultFetchWithTimeout,
64
58
  getDefaultBranch: defaultGetDefaultBranch
65
59
  },
@@ -67,11 +61,7 @@ export class TddService {
67
61
  loadBaselineMetadata: defaultLoadBaselineMetadata,
68
62
  saveBaselineMetadata: defaultSaveBaselineMetadata,
69
63
  createEmptyBaselineMetadata: defaultCreateEmptyBaselineMetadata,
70
- upsertScreenshotInMetadata: defaultUpsertScreenshotInMetadata,
71
- loadHotspotMetadata: defaultLoadHotspotMetadata,
72
- saveHotspotMetadata: defaultSaveHotspotMetadata,
73
- loadRegionMetadata: defaultLoadRegionMetadata,
74
- saveRegionMetadata: defaultSaveRegionMetadata
64
+ upsertScreenshotInMetadata: defaultUpsertScreenshotInMetadata
75
65
  },
76
66
  baseline: {
77
67
  baselineExists: defaultBaselineExists,
@@ -139,12 +129,6 @@ export class TddService {
139
129
  this.minClusterSize = config.comparison?.minClusterSize ?? 2;
140
130
  this.signatureProperties = config.signatureProperties ?? [];
141
131
 
142
- // Hotspot data (loaded lazily from disk or downloaded from cloud)
143
- this.hotspotData = null;
144
-
145
- // Region data (user-defined 2D bounding boxes, loaded lazily)
146
- this.regionData = null;
147
-
148
132
  // Track whether results have been printed (to avoid duplicate output)
149
133
  this._resultsPrinted = false;
150
134
  if (this.setBaseline) {
@@ -170,7 +154,6 @@ export class TddService {
170
154
  getTddBaselines,
171
155
  getBuilds,
172
156
  getComparison,
173
- getComparisonContext,
174
157
  clearBaselineData,
175
158
  generateScreenshotSignature,
176
159
  generateBaselineFilename,
@@ -223,19 +206,10 @@ export class TddService {
223
206
  output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
224
207
  }
225
208
  baselineBuild.screenshots = apiResponse.screenshots;
226
- this.saveDynamicMetadataFromBaselineResponse(apiResponse);
227
209
  } else if (comparisonId) {
228
210
  // Handle specific comparison download
229
211
  output.info(`Using comparison: ${comparisonId}`);
230
212
  let comparison = await getComparison(this.client, comparisonId);
231
- let comparisonContext = null;
232
- if (!comparison.dynamic_content && getComparisonContext) {
233
- try {
234
- comparisonContext = await getComparisonContext(this.client, comparisonId);
235
- } catch (error) {
236
- output.debug('tdd', `comparison context unavailable for ${comparisonId}: ${error.message}`);
237
- }
238
- }
239
213
  if (!comparison.baseline_screenshot) {
240
214
  throw new Error(`Comparison ${comparisonId} has no baseline screenshot. This comparison may be a "new" screenshot.`);
241
215
  }
@@ -266,11 +240,6 @@ export class TddService {
266
240
  }
267
241
  }
268
242
  let screenshotName = comparison.baseline_name || comparison.current_name;
269
- let dynamicContent = comparison.dynamic_content || {
270
- hotspot_analysis: comparisonContext?.history?.hotspot_analysis,
271
- confirmed_regions: comparisonContext?.history?.confirmed_regions
272
- };
273
- this.saveDynamicMetadataForScreenshot(screenshotName, dynamicContent);
274
243
  let signature = generateScreenshotSignature(screenshotName, screenshotProperties, this.signatureProperties);
275
244
  let filename = generateBaselineFilename(screenshotName, signature);
276
245
  baselineBuild = {
@@ -310,7 +279,6 @@ export class TddService {
310
279
  }
311
280
  baselineBuild = apiResponse.build;
312
281
  baselineBuild.screenshots = apiResponse.screenshots;
313
- this.saveDynamicMetadataFromBaselineResponse(apiResponse);
314
282
  }
315
283
  let buildDetails = baselineBuild;
316
284
  if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
@@ -436,7 +404,7 @@ export class TddService {
436
404
  buildInfo: {
437
405
  commitSha: baselineBuild.commit_sha,
438
406
  commitMessage: baselineBuild.commit_message,
439
- approvalStatus: baselineBuild.approval_status,
407
+ visual_review: baselineBuild.visual_review || null,
440
408
  completedAt: baselineBuild.completed_at
441
409
  },
442
410
  screenshots: buildDetails.screenshots.filter(s => s.filename).map(s => ({
@@ -458,9 +426,6 @@ export class TddService {
458
426
  };
459
427
  saveBaselineMetadata(this.baselinePath, this.baselineData);
460
428
 
461
- // Hotspots and regions are now bundled in the tdd-baselines API response
462
- // and saved earlier when processing the API response
463
-
464
429
  // Save baseline build metadata for MCP plugin
465
430
  let baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
466
431
  writeFileSync(baselineMetadataPath, JSON.stringify({
@@ -470,7 +435,7 @@ export class TddService {
470
435
  environment,
471
436
  commitSha: baselineBuild.commit_sha,
472
437
  commitMessage: baselineBuild.commit_message,
473
- approvalStatus: baselineBuild.approval_status,
438
+ visual_review: baselineBuild.visual_review || null,
474
439
  completedAt: baselineBuild.completed_at,
475
440
  downloadedAt: new Date().toISOString()
476
441
  }, null, 2));
@@ -495,49 +460,6 @@ export class TddService {
495
460
  throw error;
496
461
  }
497
462
  }
498
- saveDynamicMetadataFromBaselineResponse(apiResponse = {}) {
499
- let {
500
- saveHotspotMetadata,
501
- saveRegionMetadata
502
- } = this._deps;
503
- let hasHotspots = Object.keys(apiResponse.hotspots || {}).length > 0;
504
- let hasRegions = Object.keys(apiResponse.regions || {}).length > 0;
505
- if (hasHotspots) {
506
- this.hotspotData = apiResponse.hotspots;
507
- saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
508
- }
509
- if (hasRegions) {
510
- this.regionData = apiResponse.regions;
511
- saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
512
- }
513
- }
514
- saveDynamicMetadataForScreenshot(screenshotName, dynamicContent = {}) {
515
- if (!screenshotName) {
516
- return;
517
- }
518
- let hotspotAnalysis = dynamicContent.hotspot_analysis;
519
- let confirmedRegions = dynamicContent.confirmed_regions || [];
520
- let hasHotspotAnalysis = hotspotAnalysis?.total_builds_analyzed > 0;
521
- let hasConfirmedRegions = confirmedRegions.length > 0;
522
- let summary = {
523
- screenshots: 1,
524
- hotspots: hasHotspotAnalysis ? 1 : 0,
525
- regions: hasConfirmedRegions ? 1 : 0,
526
- total_regions: confirmedRegions.length
527
- };
528
- this.saveDynamicMetadataFromBaselineResponse({
529
- hotspots: hasHotspotAnalysis ? {
530
- [screenshotName]: hotspotAnalysis
531
- } : {},
532
- regions: hasConfirmedRegions ? {
533
- [screenshotName]: {
534
- confirmed: confirmedRegions,
535
- candidates: []
536
- }
537
- } : {},
538
- summary
539
- });
540
- }
541
463
 
542
464
  /**
543
465
  * Process already-fetched baseline data (for use when caller handles auth)
@@ -576,7 +498,6 @@ export class TddService {
576
498
  }
577
499
  }
578
500
  let baselineBuild = apiResponse.build;
579
- this.saveDynamicMetadataFromBaselineResponse(apiResponse);
580
501
  if (baselineBuild.status === 'failed') {
581
502
  output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
582
503
  return await this.handleLocalBaselines();
@@ -708,7 +629,7 @@ export class TddService {
708
629
  buildInfo: {
709
630
  commitSha: baselineBuild.commit_sha,
710
631
  commitMessage: baselineBuild.commit_message,
711
- approvalStatus: baselineBuild.approval_status,
632
+ visual_review: baselineBuild.visual_review || null,
712
633
  completedAt: baselineBuild.completed_at
713
634
  },
714
635
  screenshots: buildDetails.screenshots.filter(s => s.filename).map(s => ({
@@ -730,9 +651,6 @@ export class TddService {
730
651
  };
731
652
  saveBaselineMetadata(this.baselinePath, this.baselineData);
732
653
 
733
- // Hotspots and regions are now bundled in the tdd-baselines API response
734
- // and saved earlier when processing the API response
735
-
736
654
  // Save baseline build metadata for MCP plugin
737
655
  let baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
738
656
  writeFileSync(baselineMetadataPath, JSON.stringify({
@@ -742,7 +660,7 @@ export class TddService {
742
660
  environment: 'test',
743
661
  commitSha: baselineBuild.commit_sha,
744
662
  commitMessage: baselineBuild.commit_message,
745
- approvalStatus: baselineBuild.approval_status,
663
+ visual_review: baselineBuild.visual_review || null,
746
664
  completedAt: baselineBuild.completed_at,
747
665
  downloadedAt: new Date().toISOString()
748
666
  }, null, 2));
@@ -764,118 +682,6 @@ export class TddService {
764
682
  return this.baselineData;
765
683
  }
766
684
 
767
- /**
768
- * Download hotspot data for screenshots
769
- */
770
- async downloadHotspots(screenshots) {
771
- let {
772
- output,
773
- getBatchHotspots,
774
- saveHotspotMetadata
775
- } = this._deps;
776
- if (!this.config.apiKey) {
777
- output.debug('tdd', 'Skipping hotspot download - no API token configured');
778
- return;
779
- }
780
- try {
781
- let screenshotNames = [...new Set(screenshots.map(s => s.name))];
782
- if (screenshotNames.length === 0) {
783
- return;
784
- }
785
- output.info(`🔥 Fetching hotspot data for ${screenshotNames.length} screenshots...`);
786
- let response = await getBatchHotspots(this.client, screenshotNames);
787
- if (!response.hotspots || Object.keys(response.hotspots).length === 0) {
788
- output.debug('tdd', 'No hotspot data available from cloud');
789
- return;
790
- }
791
-
792
- // Update memory cache
793
- this.hotspotData = response.hotspots;
794
-
795
- // Save to disk using extracted module
796
- saveHotspotMetadata(this.workingDir, response.hotspots, response.summary);
797
- let hotspotCount = Object.keys(response.hotspots).length;
798
- let totalRegions = Object.values(response.hotspots).reduce((sum, h) => sum + (h.regions?.length || 0), 0);
799
- output.info(`Downloaded hotspot data for ${hotspotCount} screenshots (${totalRegions} regions total)`);
800
- } catch (error) {
801
- output.debug('tdd', `Hotspot download failed: ${error.message}`);
802
- output.warn('Could not fetch hotspot data - comparisons will run without noise filtering');
803
- }
804
- }
805
-
806
- /**
807
- * Load hotspot data from disk
808
- */
809
- loadHotspots() {
810
- let {
811
- loadHotspotMetadata
812
- } = this._deps;
813
- return loadHotspotMetadata(this.workingDir);
814
- }
815
-
816
- /**
817
- * Get hotspot for a specific screenshot
818
- *
819
- * Note: Once hotspotData is loaded (from disk or cloud), we don't reload.
820
- * This is intentional - hotspots are downloaded once per session and cached.
821
- * If a screenshot isn't in the cache, it means no hotspot data exists for it.
822
- */
823
- getHotspotForScreenshot(screenshotName) {
824
- // Check memory cache first
825
- if (this.hotspotData?.[screenshotName]) {
826
- return this.hotspotData[screenshotName];
827
- }
828
-
829
- // Try loading from disk (only if we haven't loaded yet)
830
- if (!this.hotspotData) {
831
- this.hotspotData = this.loadHotspots();
832
- }
833
- return this.hotspotData?.[screenshotName] || null;
834
- }
835
-
836
- /**
837
- * Load region data from disk
838
- */
839
- loadRegions() {
840
- let {
841
- loadRegionMetadata
842
- } = this._deps;
843
- return loadRegionMetadata(this.workingDir);
844
- }
845
-
846
- /**
847
- * Get user-defined regions for a specific screenshot
848
- *
849
- * Note: Once regionData is loaded (from disk or cloud), we don't reload.
850
- * This is intentional - regions are downloaded once per session and cached.
851
- * If a screenshot isn't in the cache, it means no region data exists for it.
852
- *
853
- * @param {string} screenshotName - Name of the screenshot
854
- * @returns {Object|null} Region data { confirmed: [], candidates: [] } or null
855
- */
856
- getRegionsForScreenshot(screenshotName) {
857
- // Check memory cache first
858
- if (this.regionData?.[screenshotName]) {
859
- return this.regionData[screenshotName];
860
- }
861
-
862
- // Try loading from disk (only if we haven't loaded yet)
863
- if (!this.regionData) {
864
- this.regionData = this.loadRegions();
865
- }
866
- return this.regionData?.[screenshotName] || null;
867
- }
868
-
869
- /**
870
- * Calculate hotspot coverage (delegating to pure function)
871
- */
872
- calculateHotspotCoverage(diffClusters, hotspotAnalysis) {
873
- let {
874
- calculateHotspotCoverage
875
- } = this._deps;
876
- return calculateHotspotCoverage(diffClusters, hotspotAnalysis);
877
- }
878
-
879
685
  /**
880
686
  * Handle local baselines logic
881
687
  */
@@ -946,7 +752,7 @@ export class TddService {
946
752
  /**
947
753
  * Compare a screenshot against baseline
948
754
  */
949
- async compareScreenshot(name, imageBuffer, properties = {}) {
755
+ async compareScreenshot(name, imageBuffer, properties = {}, screenshotOptions = {}) {
950
756
  // Destructure dependencies
951
757
  let {
952
758
  output,
@@ -1044,8 +850,8 @@ export class TddService {
1044
850
  }
1045
851
 
1046
852
  // Baseline exists - compare
1047
- let effectiveThreshold = typeof validatedProperties.threshold === 'number' && validatedProperties.threshold >= 0 ? validatedProperties.threshold : this.threshold;
1048
- let effectiveMinClusterSize = Number.isInteger(validatedProperties.minClusterSize) && validatedProperties.minClusterSize >= 1 ? validatedProperties.minClusterSize : this.minClusterSize;
853
+ let effectiveThreshold = typeof screenshotOptions.threshold === 'number' && screenshotOptions.threshold >= 0 ? screenshotOptions.threshold : this.threshold;
854
+ let effectiveMinClusterSize = Number.isInteger(screenshotOptions.minClusterSize) && screenshotOptions.minClusterSize >= 1 ? screenshotOptions.minClusterSize : this.minClusterSize;
1049
855
  try {
1050
856
  let honeydiffResult = await compareImages(baselineImagePath, currentImagePath, diffImagePath, {
1051
857
  threshold: effectiveThreshold,
@@ -1065,8 +871,6 @@ export class TddService {
1065
871
  this._upsertComparison(result);
1066
872
  return result;
1067
873
  } else {
1068
- let hotspotAnalysis = this.getHotspotForScreenshot(name);
1069
- let regionData = this.getRegionsForScreenshot(name);
1070
874
  let result = buildFailedComparison({
1071
875
  name: sanitizedName,
1072
876
  signature,
@@ -1076,9 +880,7 @@ export class TddService {
1076
880
  properties: validatedProperties,
1077
881
  threshold: effectiveThreshold,
1078
882
  minClusterSize: effectiveMinClusterSize,
1079
- honeydiffResult,
1080
- hotspotAnalysis,
1081
- regionData
883
+ honeydiffResult
1082
884
  });
1083
885
 
1084
886
  // Log at debug level only (shown with --verbose)
@@ -210,7 +210,7 @@ export interface BuildResult {
210
210
  failedComparisons?: number;
211
211
  newComparisons?: number;
212
212
  identicalComparisons?: number;
213
- approvalStatus?: string;
213
+ visual_review: unknown | null;
214
214
  url?: string;
215
215
  }
216
216
 
@@ -401,7 +401,11 @@ export interface TddService {
401
401
  compareScreenshot(
402
402
  name: string,
403
403
  imageBuffer: Buffer,
404
- properties?: Record<string, unknown>
404
+ properties?: Record<string, unknown>,
405
+ screenshotOptions?: Pick<
406
+ ScreenshotOptions,
407
+ 'threshold' | 'minClusterSize' | 'fullPage'
408
+ >
405
409
  ): Promise<ComparisonResult>;
406
410
 
407
411
  getResults(): TddResults;
@@ -340,13 +340,12 @@ export function buildUploadResult({
340
340
  * @returns {Object} Wait result
341
341
  */
342
342
  export function buildWaitResult(build) {
343
- let totalComparisons = build.comparisonsTotal ?? build.total_comparisons;
344
- let passedComparisons = build.comparisonsPassed ?? build.passed_comparisons ?? build.identicalComparisons ?? build.identical_comparisons;
345
- let failedComparisons = build.comparisonsFailed ?? build.failed_comparisons ?? build.changedComparisons ?? build.changed_comparisons;
346
- let newComparisons = build.comparisonsNew ?? build.newComparisons ?? build.new_comparisons;
347
- let identicalComparisons = build.identicalComparisons ?? build.identical_comparisons ?? passedComparisons;
348
- let approvalStatus = build.approvalStatus ?? build.approval_status;
349
- let hasComparisonCounts = build.comparisonsTotal !== undefined || build.total_comparisons !== undefined || build.comparisonsPassed !== undefined || build.passed_comparisons !== undefined || build.comparisonsFailed !== undefined || build.failed_comparisons !== undefined || build.comparisonsNew !== undefined || build.new_comparisons !== undefined || build.identicalComparisons !== undefined || build.identical_comparisons !== undefined;
343
+ let totalComparisons = build.total_comparisons;
344
+ let passedComparisons = build.identical_comparisons;
345
+ let failedComparisons = build.changed_comparisons;
346
+ let newComparisons = build.new_comparisons;
347
+ let identicalComparisons = build.identical_comparisons;
348
+ let hasComparisonCounts = build.total_comparisons !== undefined || build.new_comparisons !== undefined || build.changed_comparisons !== undefined || build.identical_comparisons !== undefined;
350
349
  let result = {
351
350
  status: build.status,
352
351
  build,
@@ -354,7 +353,7 @@ export function buildWaitResult(build) {
354
353
  failedComparisons,
355
354
  newComparisons,
356
355
  identicalComparisons,
357
- approvalStatus,
356
+ visual_review: build.visual_review || null,
358
357
  totalComparisons
359
358
  };
360
359
  if (hasComparisonCounts) {
@@ -9,16 +9,62 @@ export let RESERVED_PROPERTY_OPTIONS = Object.freeze({
9
9
  minClusterSize: {
10
10
  message: 'Move "minClusterSize" out of properties; properties is only for user metadata.'
11
11
  },
12
+ min_cluster_size: {
13
+ message: 'Move "min_cluster_size" out of properties; properties is only for user metadata.'
14
+ },
12
15
  fullPage: {
13
16
  message: 'Move "fullPage" out of properties; properties is only for user metadata.'
14
17
  },
18
+ full_page: {
19
+ message: 'Move "full_page" out of properties; properties is only for user metadata.'
20
+ },
21
+ captureMode: {
22
+ message: 'Move "captureMode" out of properties; properties is only for user metadata.'
23
+ },
24
+ capture_mode: {
25
+ message: 'Move "capture_mode" out of properties; properties is only for user metadata.'
26
+ },
27
+ deviceScaleFactor: {
28
+ message: 'Move "deviceScaleFactor" out of properties; properties is only for user metadata.'
29
+ },
30
+ device_scale_factor: {
31
+ message: 'Move "device_scale_factor" out of properties; properties is only for user metadata.'
32
+ },
33
+ pixelRatio: {
34
+ message: 'Move "pixelRatio" out of properties; properties is only for user metadata.'
35
+ },
36
+ dpr: {
37
+ message: 'Move "dpr" out of properties; properties is only for user metadata.'
38
+ },
39
+ selector: {
40
+ message: 'Move "selector" out of properties; properties is only for user metadata.'
41
+ },
42
+ component: {
43
+ message: 'Move "component" out of properties; properties is only for user metadata.'
44
+ },
45
+ elementSelector: {
46
+ message: 'Move "elementSelector" out of properties; properties is only for user metadata.'
47
+ },
48
+ element_selector: {
49
+ message: 'Move "element_selector" out of properties; properties is only for user metadata.'
50
+ },
15
51
  buildId: {
16
52
  message: 'Move "buildId" out of properties; properties is only for user metadata.'
17
53
  },
54
+ build_id: {
55
+ message: 'Move "build_id" out of properties; properties is only for user metadata.'
56
+ },
18
57
  requestTimeout: {
19
58
  message: 'Move "requestTimeout" out of properties; properties is only for user metadata.'
59
+ },
60
+ request_timeout: {
61
+ message: 'Move "request_timeout" out of properties; properties is only for user metadata.'
20
62
  }
21
63
  });
64
+ export let SCREENSHOT_OPTION_NAMES = Object.freeze(['threshold', 'minClusterSize', 'fullPage', 'captureMode', 'deviceScaleFactor', 'selector']);
65
+ export function getScreenshotOptionsPayload(options = {}) {
66
+ return Object.fromEntries(SCREENSHOT_OPTION_NAMES.filter(option => options[option] !== undefined).map(option => [option, options[option]]));
67
+ }
22
68
  function createReservedPropertyWarning(option) {
23
69
  return {
24
70
  code: 'reserved-property-option',
@@ -28,8 +74,9 @@ function createReservedPropertyWarning(option) {
28
74
  }
29
75
 
30
76
  /**
31
- * Normalize screenshot SDK options into the properties payload consumed by the
32
- * local TDD server and cloud-compatible comparison path.
77
+ * Normalize screenshot SDK options into the local and cloud upload payload.
78
+ * Reserved names found inside properties are discarded instead of being promoted
79
+ * into options.
33
80
  */
34
81
  export function normalizeScreenshotOptions(options = {}) {
35
82
  let {
@@ -38,38 +85,30 @@ export function normalizeScreenshotOptions(options = {}) {
38
85
  requestTimeout,
39
86
  threshold,
40
87
  minClusterSize,
41
- fullPage
88
+ fullPage,
89
+ captureMode,
90
+ deviceScaleFactor,
91
+ selector
42
92
  } = options;
43
93
  let warnings = [];
44
94
  let normalizedProperties = {};
45
- for (let [key, value] of Object.entries(properties || {})) {
95
+ let sourceProperties = properties && typeof properties === 'object' && !Array.isArray(properties) ? properties : {};
96
+ for (let [key, value] of Object.entries(sourceProperties)) {
46
97
  if (RESERVED_PROPERTY_OPTIONS[key]) {
47
98
  warnings.push(createReservedPropertyWarning(key));
48
- if (key === 'threshold' && threshold === undefined) threshold = value;
49
- if (key === 'minClusterSize' && minClusterSize === undefined) {
50
- minClusterSize = value;
51
- }
52
- if (key === 'fullPage' && fullPage === undefined) fullPage = value;
53
- if (key === 'buildId' && buildId === undefined) buildId = value;
54
- if (key === 'requestTimeout' && requestTimeout === undefined) {
55
- requestTimeout = value;
56
- }
57
99
  continue;
58
100
  }
59
101
  normalizedProperties[key] = value;
60
102
  }
61
- if (threshold !== undefined) {
62
- normalizedProperties.threshold = threshold;
63
- }
64
- if (minClusterSize !== undefined) {
65
- normalizedProperties.minClusterSize = minClusterSize;
66
- }
67
- if (fullPage !== undefined) {
68
- normalizedProperties.fullPage = fullPage;
69
- }
70
103
  return {
71
104
  buildId,
72
105
  requestTimeout,
106
+ threshold,
107
+ minClusterSize,
108
+ fullPage,
109
+ captureMode,
110
+ deviceScaleFactor,
111
+ selector,
73
112
  properties: normalizedProperties,
74
113
  warnings
75
114
  };