@vizzly-testing/cli 0.15.0 → 0.16.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/dist/cli.js +3 -1
- package/dist/services/api-service.js +42 -0
- package/dist/services/tdd-service.js +189 -6
- package/dist/types/services/api-service.d.ts +21 -0
- package/dist/types/services/tdd-service.d.ts +27 -0
- package/docs/plugins.md +11 -12
- package/docs/tdd-mode.md +34 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,9 @@ try {
|
|
|
50
50
|
let registerPromise = plugin.register(program, {
|
|
51
51
|
config,
|
|
52
52
|
services,
|
|
53
|
-
output
|
|
53
|
+
output,
|
|
54
|
+
// Backwards compatibility alias for plugins using old API
|
|
55
|
+
logger: output
|
|
54
56
|
});
|
|
55
57
|
let timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
|
|
56
58
|
await Promise.race([registerPromise, timeoutPromise]);
|
|
@@ -357,4 +357,46 @@ export class ApiService {
|
|
|
357
357
|
}
|
|
358
358
|
});
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get hotspot analysis for a single screenshot
|
|
363
|
+
* @param {string} screenshotName - Screenshot name to get hotspots for
|
|
364
|
+
* @param {Object} options - Optional settings
|
|
365
|
+
* @param {number} [options.windowSize=20] - Number of historical builds to analyze
|
|
366
|
+
* @returns {Promise<Object>} Hotspot analysis data
|
|
367
|
+
*/
|
|
368
|
+
async getScreenshotHotspots(screenshotName, options = {}) {
|
|
369
|
+
let {
|
|
370
|
+
windowSize = 20
|
|
371
|
+
} = options;
|
|
372
|
+
let queryParams = new URLSearchParams({
|
|
373
|
+
windowSize: String(windowSize)
|
|
374
|
+
});
|
|
375
|
+
let encodedName = encodeURIComponent(screenshotName);
|
|
376
|
+
return this.request(`/api/sdk/screenshots/${encodedName}/hotspots?${queryParams}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Batch get hotspot analysis for multiple screenshots
|
|
381
|
+
* More efficient than calling getScreenshotHotspots for each screenshot
|
|
382
|
+
* @param {string[]} screenshotNames - Array of screenshot names
|
|
383
|
+
* @param {Object} options - Optional settings
|
|
384
|
+
* @param {number} [options.windowSize=20] - Number of historical builds to analyze
|
|
385
|
+
* @returns {Promise<Object>} Hotspots keyed by screenshot name
|
|
386
|
+
*/
|
|
387
|
+
async getBatchHotspots(screenshotNames, options = {}) {
|
|
388
|
+
let {
|
|
389
|
+
windowSize = 20
|
|
390
|
+
} = options;
|
|
391
|
+
return this.request('/api/sdk/screenshots/hotspots', {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: {
|
|
394
|
+
'Content-Type': 'application/json'
|
|
395
|
+
},
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
screenshot_names: screenshotNames,
|
|
398
|
+
windowSize
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
}
|
|
360
402
|
}
|
|
@@ -420,6 +420,9 @@ export class TddService {
|
|
|
420
420
|
const metadataPath = join(this.baselinePath, 'metadata.json');
|
|
421
421
|
writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
|
|
422
422
|
|
|
423
|
+
// Download hotspot data for noise filtering
|
|
424
|
+
await this.downloadHotspots(buildDetails.screenshots);
|
|
425
|
+
|
|
423
426
|
// Save baseline build metadata for MCP plugin
|
|
424
427
|
const baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
|
|
425
428
|
const buildMetadata = {
|
|
@@ -459,6 +462,152 @@ export class TddService {
|
|
|
459
462
|
}
|
|
460
463
|
}
|
|
461
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Download hotspot data for screenshots from the cloud
|
|
467
|
+
* Hotspots identify regions that frequently change (timestamps, IDs, etc.)
|
|
468
|
+
* Used to filter out known dynamic content during comparisons
|
|
469
|
+
* @param {Array} screenshots - Array of screenshot objects with name property
|
|
470
|
+
*/
|
|
471
|
+
async downloadHotspots(screenshots) {
|
|
472
|
+
// Only attempt if we have an API token
|
|
473
|
+
if (!this.config.apiKey) {
|
|
474
|
+
output.debug('tdd', 'Skipping hotspot download - no API token configured');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
// Get unique screenshot names
|
|
479
|
+
let screenshotNames = [...new Set(screenshots.map(s => s.name))];
|
|
480
|
+
if (screenshotNames.length === 0) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
output.info(`🔥 Fetching hotspot data for ${screenshotNames.length} screenshots...`);
|
|
484
|
+
|
|
485
|
+
// Use batch endpoint for efficiency
|
|
486
|
+
let response = await this.api.getBatchHotspots(screenshotNames);
|
|
487
|
+
if (!response.hotspots || Object.keys(response.hotspots).length === 0) {
|
|
488
|
+
output.debug('tdd', 'No hotspot data available from cloud');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Store hotspots in a separate file for easy access during comparisons
|
|
493
|
+
this.hotspotData = response.hotspots;
|
|
494
|
+
let hotspotsPath = safePath(this.workingDir, '.vizzly', 'hotspots.json');
|
|
495
|
+
writeFileSync(hotspotsPath, JSON.stringify({
|
|
496
|
+
downloadedAt: new Date().toISOString(),
|
|
497
|
+
summary: response.summary,
|
|
498
|
+
hotspots: response.hotspots
|
|
499
|
+
}, null, 2));
|
|
500
|
+
let hotspotCount = Object.keys(response.hotspots).length;
|
|
501
|
+
let totalRegions = Object.values(response.hotspots).reduce((sum, h) => sum + (h.regions?.length || 0), 0);
|
|
502
|
+
output.info(`✅ Downloaded hotspot data for ${hotspotCount} screenshots (${totalRegions} regions total)`);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
// Don't fail baseline download if hotspot fetch fails
|
|
505
|
+
output.debug('tdd', `Hotspot download failed: ${error.message}`);
|
|
506
|
+
output.warn('⚠️ Could not fetch hotspot data - comparisons will run without noise filtering');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Load hotspot data from disk
|
|
512
|
+
* @returns {Object|null} Hotspot data keyed by screenshot name, or null if not available
|
|
513
|
+
*/
|
|
514
|
+
loadHotspots() {
|
|
515
|
+
try {
|
|
516
|
+
let hotspotsPath = safePath(this.workingDir, '.vizzly', 'hotspots.json');
|
|
517
|
+
if (!existsSync(hotspotsPath)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
let data = JSON.parse(readFileSync(hotspotsPath, 'utf8'));
|
|
521
|
+
return data.hotspots || null;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
output.debug('tdd', `Failed to load hotspots: ${error.message}`);
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get hotspot analysis for a specific screenshot
|
|
530
|
+
* @param {string} screenshotName - Name of the screenshot
|
|
531
|
+
* @returns {Object|null} Hotspot analysis or null if not available
|
|
532
|
+
*/
|
|
533
|
+
getHotspotForScreenshot(screenshotName) {
|
|
534
|
+
// Check memory cache first
|
|
535
|
+
if (this.hotspotData && this.hotspotData[screenshotName]) {
|
|
536
|
+
return this.hotspotData[screenshotName];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Try loading from disk
|
|
540
|
+
if (!this.hotspotData) {
|
|
541
|
+
this.hotspotData = this.loadHotspots();
|
|
542
|
+
}
|
|
543
|
+
return this.hotspotData?.[screenshotName] || null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Calculate what percentage of diff falls within hotspot regions
|
|
548
|
+
* Uses 1D Y-coordinate matching (same algorithm as cloud)
|
|
549
|
+
* @param {Array} diffClusters - Array of diff clusters from honeydiff
|
|
550
|
+
* @param {Object} hotspotAnalysis - Hotspot data with regions array
|
|
551
|
+
* @returns {Object} Coverage info { coverage, linesInHotspots, totalLines }
|
|
552
|
+
*/
|
|
553
|
+
calculateHotspotCoverage(diffClusters, hotspotAnalysis) {
|
|
554
|
+
if (!diffClusters || diffClusters.length === 0) {
|
|
555
|
+
return {
|
|
556
|
+
coverage: 0,
|
|
557
|
+
linesInHotspots: 0,
|
|
558
|
+
totalLines: 0
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (!hotspotAnalysis || !hotspotAnalysis.regions || hotspotAnalysis.regions.length === 0) {
|
|
562
|
+
return {
|
|
563
|
+
coverage: 0,
|
|
564
|
+
linesInHotspots: 0,
|
|
565
|
+
totalLines: 0
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Extract Y-coordinates (diff lines) from clusters
|
|
570
|
+
// Each cluster has a boundingBox with y and height
|
|
571
|
+
let diffLines = [];
|
|
572
|
+
for (let cluster of diffClusters) {
|
|
573
|
+
if (cluster.boundingBox) {
|
|
574
|
+
let {
|
|
575
|
+
y,
|
|
576
|
+
height
|
|
577
|
+
} = cluster.boundingBox;
|
|
578
|
+
// Add all Y lines covered by this cluster
|
|
579
|
+
for (let line = y; line < y + height; line++) {
|
|
580
|
+
diffLines.push(line);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (diffLines.length === 0) {
|
|
585
|
+
return {
|
|
586
|
+
coverage: 0,
|
|
587
|
+
linesInHotspots: 0,
|
|
588
|
+
totalLines: 0
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Remove duplicates and sort
|
|
593
|
+
diffLines = [...new Set(diffLines)].sort((a, b) => a - b);
|
|
594
|
+
|
|
595
|
+
// Check how many diff lines fall within hotspot regions
|
|
596
|
+
let linesInHotspots = 0;
|
|
597
|
+
for (let line of diffLines) {
|
|
598
|
+
let inHotspot = hotspotAnalysis.regions.some(region => line >= region.y1 && line <= region.y2);
|
|
599
|
+
if (inHotspot) {
|
|
600
|
+
linesInHotspots++;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
let coverage = linesInHotspots / diffLines.length;
|
|
604
|
+
return {
|
|
605
|
+
coverage,
|
|
606
|
+
linesInHotspots,
|
|
607
|
+
totalLines: diffLines.length
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
462
611
|
/**
|
|
463
612
|
* Download baselines using OAuth authentication
|
|
464
613
|
* Used when user is logged in via device flow but no API token is configured
|
|
@@ -784,17 +933,36 @@ export class TddService {
|
|
|
784
933
|
this.comparisons.push(comparison);
|
|
785
934
|
return comparison;
|
|
786
935
|
} else {
|
|
787
|
-
// Images differ
|
|
936
|
+
// Images differ - check if differences are in known hotspot regions
|
|
937
|
+
let hotspotAnalysis = this.getHotspotForScreenshot(name);
|
|
938
|
+
let hotspotCoverage = null;
|
|
939
|
+
let isHotspotFiltered = false;
|
|
940
|
+
if (hotspotAnalysis && result.diffClusters && result.diffClusters.length > 0) {
|
|
941
|
+
hotspotCoverage = this.calculateHotspotCoverage(result.diffClusters, hotspotAnalysis);
|
|
942
|
+
|
|
943
|
+
// Consider it filtered if:
|
|
944
|
+
// 1. High confidence hotspot data (score >= 70)
|
|
945
|
+
// 2. 80%+ of the diff is within hotspot regions
|
|
946
|
+
let isHighConfidence = hotspotAnalysis.confidence === 'high' || hotspotAnalysis.confidence_score && hotspotAnalysis.confidence_score >= 70;
|
|
947
|
+
if (isHighConfidence && hotspotCoverage.coverage >= 0.8) {
|
|
948
|
+
isHotspotFiltered = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
788
951
|
let diffInfo = ` (${result.diffPercentage.toFixed(2)}% different, ${result.diffPixels} pixels)`;
|
|
789
952
|
|
|
790
953
|
// Add cluster info to log if available
|
|
791
954
|
if (result.diffClusters && result.diffClusters.length > 0) {
|
|
792
955
|
diffInfo += `, ${result.diffClusters.length} region${result.diffClusters.length > 1 ? 's' : ''}`;
|
|
793
956
|
}
|
|
957
|
+
|
|
958
|
+
// Add hotspot info if applicable
|
|
959
|
+
if (hotspotCoverage && hotspotCoverage.coverage > 0) {
|
|
960
|
+
diffInfo += `, ${Math.round(hotspotCoverage.coverage * 100)}% in hotspots`;
|
|
961
|
+
}
|
|
794
962
|
const comparison = {
|
|
795
963
|
id: generateComparisonId(signature),
|
|
796
964
|
name: sanitizedName,
|
|
797
|
-
status: 'failed',
|
|
965
|
+
status: isHotspotFiltered ? 'passed' : 'failed',
|
|
798
966
|
baseline: baselineImagePath,
|
|
799
967
|
current: currentImagePath,
|
|
800
968
|
diff: diffImagePath,
|
|
@@ -803,7 +971,7 @@ export class TddService {
|
|
|
803
971
|
threshold: this.threshold,
|
|
804
972
|
diffPercentage: result.diffPercentage,
|
|
805
973
|
diffCount: result.diffPixels,
|
|
806
|
-
reason: 'pixel-diff',
|
|
974
|
+
reason: isHotspotFiltered ? 'hotspot-filtered' : 'pixel-diff',
|
|
807
975
|
// Honeydiff metrics
|
|
808
976
|
totalPixels: result.totalPixels,
|
|
809
977
|
aaPixelsIgnored: result.aaPixelsIgnored,
|
|
@@ -811,10 +979,25 @@ export class TddService {
|
|
|
811
979
|
boundingBox: result.boundingBox,
|
|
812
980
|
heightDiff: result.heightDiff,
|
|
813
981
|
intensityStats: result.intensityStats,
|
|
814
|
-
diffClusters: result.diffClusters
|
|
982
|
+
diffClusters: result.diffClusters,
|
|
983
|
+
// Hotspot analysis data
|
|
984
|
+
hotspotAnalysis: hotspotCoverage ? {
|
|
985
|
+
coverage: hotspotCoverage.coverage,
|
|
986
|
+
linesInHotspots: hotspotCoverage.linesInHotspots,
|
|
987
|
+
totalLines: hotspotCoverage.totalLines,
|
|
988
|
+
confidence: hotspotAnalysis?.confidence,
|
|
989
|
+
confidenceScore: hotspotAnalysis?.confidence_score,
|
|
990
|
+
regionCount: hotspotAnalysis?.regions?.length || 0,
|
|
991
|
+
isFiltered: isHotspotFiltered
|
|
992
|
+
} : null
|
|
815
993
|
};
|
|
816
|
-
|
|
817
|
-
|
|
994
|
+
if (isHotspotFiltered) {
|
|
995
|
+
output.info(`✅ ${colors.green('PASSED')} ${sanitizedName} - differences in known hotspots${diffInfo}`);
|
|
996
|
+
output.debug('tdd', `Hotspot filtered: ${Math.round(hotspotCoverage.coverage * 100)}% coverage, confidence: ${hotspotAnalysis.confidence}`);
|
|
997
|
+
} else {
|
|
998
|
+
output.warn(`❌ ${colors.red('FAILED')} ${sanitizedName} - differences detected${diffInfo}`);
|
|
999
|
+
output.info(` Diff saved to: ${diffImagePath}`);
|
|
1000
|
+
}
|
|
818
1001
|
this.comparisons.push(comparison);
|
|
819
1002
|
return comparison;
|
|
820
1003
|
}
|
|
@@ -97,4 +97,25 @@ export class ApiService {
|
|
|
97
97
|
* @returns {Promise<Object>} Finalization result
|
|
98
98
|
*/
|
|
99
99
|
finalizeParallelBuild(parallelId: string): Promise<any>;
|
|
100
|
+
/**
|
|
101
|
+
* Get hotspot analysis for a single screenshot
|
|
102
|
+
* @param {string} screenshotName - Screenshot name to get hotspots for
|
|
103
|
+
* @param {Object} options - Optional settings
|
|
104
|
+
* @param {number} [options.windowSize=20] - Number of historical builds to analyze
|
|
105
|
+
* @returns {Promise<Object>} Hotspot analysis data
|
|
106
|
+
*/
|
|
107
|
+
getScreenshotHotspots(screenshotName: string, options?: {
|
|
108
|
+
windowSize?: number;
|
|
109
|
+
}): Promise<any>;
|
|
110
|
+
/**
|
|
111
|
+
* Batch get hotspot analysis for multiple screenshots
|
|
112
|
+
* More efficient than calling getScreenshotHotspots for each screenshot
|
|
113
|
+
* @param {string[]} screenshotNames - Array of screenshot names
|
|
114
|
+
* @param {Object} options - Optional settings
|
|
115
|
+
* @param {number} [options.windowSize=20] - Number of historical builds to analyze
|
|
116
|
+
* @returns {Promise<Object>} Hotspots keyed by screenshot name
|
|
117
|
+
*/
|
|
118
|
+
getBatchHotspots(screenshotNames: string[], options?: {
|
|
119
|
+
windowSize?: number;
|
|
120
|
+
}): Promise<any>;
|
|
100
121
|
}
|
|
@@ -16,6 +16,33 @@ export class TddService {
|
|
|
16
16
|
comparisons: any[];
|
|
17
17
|
threshold: any;
|
|
18
18
|
downloadBaselines(environment?: string, branch?: any, buildId?: any, comparisonId?: any): Promise<any>;
|
|
19
|
+
/**
|
|
20
|
+
* Download hotspot data for screenshots from the cloud
|
|
21
|
+
* Hotspots identify regions that frequently change (timestamps, IDs, etc.)
|
|
22
|
+
* Used to filter out known dynamic content during comparisons
|
|
23
|
+
* @param {Array} screenshots - Array of screenshot objects with name property
|
|
24
|
+
*/
|
|
25
|
+
downloadHotspots(screenshots: any[]): Promise<void>;
|
|
26
|
+
hotspotData: any;
|
|
27
|
+
/**
|
|
28
|
+
* Load hotspot data from disk
|
|
29
|
+
* @returns {Object|null} Hotspot data keyed by screenshot name, or null if not available
|
|
30
|
+
*/
|
|
31
|
+
loadHotspots(): any | null;
|
|
32
|
+
/**
|
|
33
|
+
* Get hotspot analysis for a specific screenshot
|
|
34
|
+
* @param {string} screenshotName - Name of the screenshot
|
|
35
|
+
* @returns {Object|null} Hotspot analysis or null if not available
|
|
36
|
+
*/
|
|
37
|
+
getHotspotForScreenshot(screenshotName: string): any | null;
|
|
38
|
+
/**
|
|
39
|
+
* Calculate what percentage of diff falls within hotspot regions
|
|
40
|
+
* Uses 1D Y-coordinate matching (same algorithm as cloud)
|
|
41
|
+
* @param {Array} diffClusters - Array of diff clusters from honeydiff
|
|
42
|
+
* @param {Object} hotspotAnalysis - Hotspot data with regions array
|
|
43
|
+
* @returns {Object} Coverage info { coverage, linesInHotspots, totalLines }
|
|
44
|
+
*/
|
|
45
|
+
calculateHotspotCoverage(diffClusters: any[], hotspotAnalysis: any): any;
|
|
19
46
|
/**
|
|
20
47
|
* Download baselines using OAuth authentication
|
|
21
48
|
* Used when user is logged in via device flow but no API token is configured
|
package/docs/plugins.md
CHANGED
|
@@ -356,7 +356,7 @@ register(program, { output }) {
|
|
|
356
356
|
export default {
|
|
357
357
|
name: 'hello',
|
|
358
358
|
version: '1.0.0',
|
|
359
|
-
register(program, {
|
|
359
|
+
register(program, { output }) {
|
|
360
360
|
program
|
|
361
361
|
.command('hello <name>')
|
|
362
362
|
.description('Say hello')
|
|
@@ -366,7 +366,7 @@ export default {
|
|
|
366
366
|
if (options.loud) {
|
|
367
367
|
greeting = greeting.toUpperCase();
|
|
368
368
|
}
|
|
369
|
-
|
|
369
|
+
output.info(greeting);
|
|
370
370
|
});
|
|
371
371
|
}
|
|
372
372
|
};
|
|
@@ -378,13 +378,13 @@ export default {
|
|
|
378
378
|
export default {
|
|
379
379
|
name: 'storybook',
|
|
380
380
|
version: '1.0.0',
|
|
381
|
-
register(program, { config,
|
|
381
|
+
register(program, { config, output, services }) {
|
|
382
382
|
program
|
|
383
383
|
.command('storybook <path>')
|
|
384
384
|
.description('Capture screenshots from Storybook build')
|
|
385
385
|
.option('--viewports <list>', 'Comma-separated viewports', '1280x720')
|
|
386
386
|
.action(async (path, options) => {
|
|
387
|
-
|
|
387
|
+
output.info(`Crawling Storybook at ${path}`);
|
|
388
388
|
|
|
389
389
|
// Import dependencies lazily
|
|
390
390
|
let { crawlStorybook } = await import('./crawler.js');
|
|
@@ -392,16 +392,15 @@ export default {
|
|
|
392
392
|
// Capture screenshots
|
|
393
393
|
let screenshots = await crawlStorybook(path, {
|
|
394
394
|
viewports: options.viewports.split(','),
|
|
395
|
-
logger,
|
|
396
395
|
});
|
|
397
396
|
|
|
398
|
-
|
|
397
|
+
output.info(`Captured ${screenshots.length} screenshots`);
|
|
399
398
|
|
|
400
399
|
// Upload using Vizzly's uploader service
|
|
401
400
|
let uploader = await services.get('uploader');
|
|
402
401
|
await uploader.uploadScreenshots(screenshots);
|
|
403
402
|
|
|
404
|
-
|
|
403
|
+
output.success('Upload complete!');
|
|
405
404
|
});
|
|
406
405
|
}
|
|
407
406
|
};
|
|
@@ -413,7 +412,7 @@ export default {
|
|
|
413
412
|
export default {
|
|
414
413
|
name: 'reports',
|
|
415
414
|
version: '1.0.0',
|
|
416
|
-
register(program, {
|
|
415
|
+
register(program, { output }) {
|
|
417
416
|
let reports = program
|
|
418
417
|
.command('reports')
|
|
419
418
|
.description('Report generation commands');
|
|
@@ -422,14 +421,14 @@ export default {
|
|
|
422
421
|
.command('generate')
|
|
423
422
|
.description('Generate a new report')
|
|
424
423
|
.action(() => {
|
|
425
|
-
|
|
424
|
+
output.info('Generating report...');
|
|
426
425
|
});
|
|
427
426
|
|
|
428
427
|
reports
|
|
429
428
|
.command('list')
|
|
430
429
|
.description('List all reports')
|
|
431
430
|
.action(() => {
|
|
432
|
-
|
|
431
|
+
output.info('Listing reports...');
|
|
433
432
|
});
|
|
434
433
|
}
|
|
435
434
|
};
|
|
@@ -474,10 +473,10 @@ If you're using TypeScript or want better IDE support, you can add JSDoc types:
|
|
|
474
473
|
* @param {import('commander').Command} program
|
|
475
474
|
* @param {Object} context
|
|
476
475
|
* @param {Object} context.config
|
|
477
|
-
* @param {Object} context.
|
|
476
|
+
* @param {Object} context.output
|
|
478
477
|
* @param {Object} context.services
|
|
479
478
|
*/
|
|
480
|
-
function register(program, { config,
|
|
479
|
+
function register(program, { config, output, services }) {
|
|
481
480
|
// Your plugin code with full autocomplete!
|
|
482
481
|
}
|
|
483
482
|
|
package/docs/tdd-mode.md
CHANGED
|
@@ -311,6 +311,40 @@ npx vizzly tdd run "npm test"
|
|
|
311
311
|
npx vizzly run "npm test" --build-name "Fix: Header alignment issue"
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
+
## Hotspot Filtering
|
|
315
|
+
|
|
316
|
+
When connected to Vizzly cloud, TDD mode automatically filters out "noise" from known hotspot areas - regions that frequently change across builds (like timestamps, animations, or dynamic content).
|
|
317
|
+
|
|
318
|
+
### How It Works
|
|
319
|
+
|
|
320
|
+
1. **Download baselines** - Use the TDD dashboard's Builds page to download baselines from the cloud (hotspot data is included automatically)
|
|
321
|
+
2. **Automatic filtering** - During comparisons, if a diff falls within a known hotspot region, it's automatically marked as passed
|
|
322
|
+
3. **Visual feedback** - You'll see output like:
|
|
323
|
+
```
|
|
324
|
+
✅ PASSED Dashboard - differences in known hotspots (0.15% different, 42 pixels, 1 region, 95% in hotspots)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Requirements
|
|
328
|
+
|
|
329
|
+
Hotspot filtering activates automatically when:
|
|
330
|
+
- You have an API token configured (`vizzly login` or `VIZZLY_TOKEN`)
|
|
331
|
+
- You've downloaded baselines from the cloud (via the TDD dashboard's Builds page)
|
|
332
|
+
- The cloud has enough historical build data to calculate hotspot regions
|
|
333
|
+
|
|
334
|
+
### Filtering Criteria
|
|
335
|
+
|
|
336
|
+
A diff is filtered (auto-passed) when:
|
|
337
|
+
- **80%+ of the diff** falls within known hotspot regions
|
|
338
|
+
- **High confidence** hotspot data (confidence score ≥ 70)
|
|
339
|
+
|
|
340
|
+
If the diff falls outside hotspots or confidence is low, the comparison fails normally so you can review it.
|
|
341
|
+
|
|
342
|
+
### Benefits
|
|
343
|
+
|
|
344
|
+
- **Reduced noise** - Stop seeing the same timestamp/animation diffs over and over
|
|
345
|
+
- **Faster reviews** - Focus on real visual changes, not known dynamic areas
|
|
346
|
+
- **Smart detection** - Hotspots are calculated from your actual build history, not manual configuration
|
|
347
|
+
|
|
314
348
|
## Comparison Settings
|
|
315
349
|
|
|
316
350
|
TDD Mode uses the same comparison settings as production:
|