@vizzly-testing/cli 0.15.1 → 0.16.1
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/client/index.js +21 -4
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +11 -11
- package/dist/sdk/index.js +6 -6
- package/dist/services/api-service.js +42 -0
- package/dist/services/tdd-service.js +189 -6
- package/dist/services/test-runner.js +0 -8
- package/dist/services/uploader.js +1 -1
- package/dist/types/client.d.ts +88 -0
- package/dist/types/config.d.ts +29 -0
- package/dist/types/index.d.ts +426 -9
- package/dist/types/sdk.d.ts +146 -0
- package/dist/utils/config-helpers.js +2 -2
- package/docs/tdd-mode.md +34 -0
- package/package.json +21 -4
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/client/index.d.ts +0 -80
- package/dist/types/commands/doctor.d.ts +0 -11
- package/dist/types/commands/finalize.d.ts +0 -13
- package/dist/types/commands/init.d.ts +0 -32
- package/dist/types/commands/login.d.ts +0 -11
- package/dist/types/commands/logout.d.ts +0 -11
- package/dist/types/commands/project.d.ts +0 -28
- package/dist/types/commands/run.d.ts +0 -16
- package/dist/types/commands/status.d.ts +0 -13
- package/dist/types/commands/tdd-daemon.d.ts +0 -24
- package/dist/types/commands/tdd.d.ts +0 -17
- package/dist/types/commands/upload.d.ts +0 -13
- package/dist/types/commands/whoami.d.ts +0 -11
- package/dist/types/errors/vizzly-error.d.ts +0 -75
- package/dist/types/plugin-loader.d.ts +0 -7
- package/dist/types/reporter/src/api/client.d.ts +0 -178
- package/dist/types/reporter/src/components/app-router.d.ts +0 -1
- package/dist/types/reporter/src/components/code-block.d.ts +0 -4
- package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +0 -5
- package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +0 -7
- package/dist/types/reporter/src/components/comparison/comparison-group.d.ts +0 -10
- package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +0 -6
- package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +0 -10
- package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +0 -11
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +0 -14
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +0 -30
- package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +0 -8
- package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +0 -8
- package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +0 -13
- package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +0 -16
- package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +0 -9
- package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +0 -9
- package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +0 -4
- package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +0 -16
- package/dist/types/reporter/src/components/design-system/alert.d.ts +0 -9
- package/dist/types/reporter/src/components/design-system/badge.d.ts +0 -17
- package/dist/types/reporter/src/components/design-system/button.d.ts +0 -19
- package/dist/types/reporter/src/components/design-system/card.d.ts +0 -31
- package/dist/types/reporter/src/components/design-system/empty-state.d.ts +0 -13
- package/dist/types/reporter/src/components/design-system/form-controls.d.ts +0 -44
- package/dist/types/reporter/src/components/design-system/health-ring.d.ts +0 -7
- package/dist/types/reporter/src/components/design-system/index.d.ts +0 -11
- package/dist/types/reporter/src/components/design-system/modal.d.ts +0 -10
- package/dist/types/reporter/src/components/design-system/skeleton.d.ts +0 -19
- package/dist/types/reporter/src/components/design-system/spinner.d.ts +0 -10
- package/dist/types/reporter/src/components/design-system/tabs.d.ts +0 -13
- package/dist/types/reporter/src/components/layout/header.d.ts +0 -5
- package/dist/types/reporter/src/components/layout/index.d.ts +0 -2
- package/dist/types/reporter/src/components/layout/layout.d.ts +0 -6
- package/dist/types/reporter/src/components/ui/smart-image.d.ts +0 -7
- package/dist/types/reporter/src/components/ui/toast.d.ts +0 -4
- package/dist/types/reporter/src/components/views/builds-view.d.ts +0 -1
- package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +0 -5
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +0 -5
- package/dist/types/reporter/src/components/views/projects-view.d.ts +0 -1
- package/dist/types/reporter/src/components/views/settings-view.d.ts +0 -1
- package/dist/types/reporter/src/components/views/stats-view.d.ts +0 -1
- package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +0 -1
- package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +0 -15
- package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +0 -6
- package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +0 -6
- package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +0 -9
- package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +0 -20
- package/dist/types/reporter/src/hooks/use-image-loader.d.ts +0 -1
- package/dist/types/reporter/src/lib/query-client.d.ts +0 -2
- package/dist/types/reporter/src/lib/query-keys.d.ts +0 -13
- package/dist/types/reporter/src/main.d.ts +0 -1
- package/dist/types/reporter/src/services/api-client.d.ts +0 -11
- package/dist/types/reporter/src/utils/comparison-helpers.d.ts +0 -16
- package/dist/types/reporter/src/utils/constants.d.ts +0 -37
- package/dist/types/reporter/vite.config.d.ts +0 -2
- package/dist/types/reporter/vite.dev.config.d.ts +0 -2
- package/dist/types/sdk/index.d.ts +0 -110
- package/dist/types/server/handlers/api-handler.d.ts +0 -88
- package/dist/types/server/handlers/tdd-handler.d.ts +0 -162
- package/dist/types/server/http-server.d.ts +0 -6
- package/dist/types/server/middleware/cors.d.ts +0 -11
- package/dist/types/server/middleware/json-parser.d.ts +0 -10
- package/dist/types/server/middleware/response.d.ts +0 -50
- package/dist/types/server/routers/assets.d.ts +0 -6
- package/dist/types/server/routers/auth.d.ts +0 -9
- package/dist/types/server/routers/baseline.d.ts +0 -13
- package/dist/types/server/routers/cloud-proxy.d.ts +0 -11
- package/dist/types/server/routers/config.d.ts +0 -9
- package/dist/types/server/routers/dashboard.d.ts +0 -6
- package/dist/types/server/routers/health.d.ts +0 -11
- package/dist/types/server/routers/projects.d.ts +0 -9
- package/dist/types/server/routers/screenshot.d.ts +0 -11
- package/dist/types/services/api-service.d.ts +0 -100
- package/dist/types/services/auth-service.d.ts +0 -59
- package/dist/types/services/build-manager.d.ts +0 -69
- package/dist/types/services/config-service.d.ts +0 -97
- package/dist/types/services/html-report-generator.d.ts +0 -52
- package/dist/types/services/index.d.ts +0 -7
- package/dist/types/services/project-service.d.ts +0 -105
- package/dist/types/services/report-generator/viewer.d.ts +0 -0
- package/dist/types/services/screenshot-server.d.ts +0 -10
- package/dist/types/services/server-manager.d.ts +0 -269
- package/dist/types/services/static-report-generator.d.ts +0 -25
- package/dist/types/services/tdd-service.d.ts +0 -95
- package/dist/types/services/test-runner.d.ts +0 -31
- package/dist/types/services/uploader.d.ts +0 -36
- package/dist/types/utils/browser.d.ts +0 -6
- package/dist/types/utils/build-history.d.ts +0 -16
- package/dist/types/utils/ci-env.d.ts +0 -55
- package/dist/types/utils/colors.d.ts +0 -12
- package/dist/types/utils/config-helpers.d.ts +0 -6
- package/dist/types/utils/config-loader.d.ts +0 -25
- package/dist/types/utils/config-schema.d.ts +0 -51
- package/dist/types/utils/environment-config.d.ts +0 -59
- package/dist/types/utils/environment.d.ts +0 -36
- package/dist/types/utils/fetch-utils.d.ts +0 -1
- package/dist/types/utils/file-helpers.d.ts +0 -18
- package/dist/types/utils/git.d.ts +0 -56
- package/dist/types/utils/global-config.d.ts +0 -84
- package/dist/types/utils/image-input-detector.d.ts +0 -71
- package/dist/types/utils/output.d.ts +0 -84
- package/dist/types/utils/package-info.d.ts +0 -15
- package/dist/types/utils/security.d.ts +0 -30
package/dist/sdk/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { VizzlyError } from '../errors/vizzly-error.js';
|
|
|
22
22
|
/**
|
|
23
23
|
* Create a new Vizzly instance with custom configuration
|
|
24
24
|
*
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {Object} [config] - Configuration options
|
|
26
26
|
* @returns {Promise<VizzlySDK>} Configured Vizzly SDK instance
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
@@ -147,7 +147,7 @@ export function createVizzly(config = {}, options = {}) {
|
|
|
147
147
|
*/
|
|
148
148
|
export class VizzlySDK extends EventEmitter {
|
|
149
149
|
/**
|
|
150
|
-
* @param {
|
|
150
|
+
* @param {Object} config - Configuration
|
|
151
151
|
* @param {Object} services - Service instances
|
|
152
152
|
*/
|
|
153
153
|
constructor(config, services) {
|
|
@@ -223,7 +223,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
223
223
|
* Capture a screenshot
|
|
224
224
|
* @param {string} name - Screenshot name
|
|
225
225
|
* @param {Buffer|string} imageBuffer - Image data as a Buffer, or a file path to an image
|
|
226
|
-
* @param {
|
|
226
|
+
* @param {Object} [options] - Options
|
|
227
227
|
* @returns {Promise<void>}
|
|
228
228
|
* @throws {VizzlyError} When server is not running
|
|
229
229
|
* @throws {VizzlyError} When file path is provided but file doesn't exist
|
|
@@ -288,8 +288,8 @@ export class VizzlySDK extends EventEmitter {
|
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
290
|
* Upload all captured screenshots
|
|
291
|
-
* @param {
|
|
292
|
-
* @returns {Promise<
|
|
291
|
+
* @param {Object} [options] - Upload options
|
|
292
|
+
* @returns {Promise<Object>} Upload result
|
|
293
293
|
*/
|
|
294
294
|
async upload(options = {}) {
|
|
295
295
|
if (!this.services?.uploader) {
|
|
@@ -332,7 +332,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
332
332
|
* Run local comparison in TDD mode
|
|
333
333
|
* @param {string} name - Screenshot name
|
|
334
334
|
* @param {Buffer|string} imageBuffer - Current image as a Buffer, or a file path to an image
|
|
335
|
-
* @returns {Promise<
|
|
335
|
+
* @returns {Promise<Object>} Comparison result
|
|
336
336
|
* @throws {VizzlyError} When file path is provided but file doesn't exist
|
|
337
337
|
* @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
|
|
338
338
|
*/
|
|
@@ -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
|
}
|
|
@@ -94,14 +94,6 @@ export class TestRunner extends EventEmitter {
|
|
|
94
94
|
|
|
95
95
|
// Start server with appropriate handler
|
|
96
96
|
await this.serverManager.start(buildId, tdd, options.setBaseline);
|
|
97
|
-
|
|
98
|
-
// Forward server events
|
|
99
|
-
if (this.serverManager.server?.emitter) {
|
|
100
|
-
this.serverManager.server.emitter.on('screenshot-captured', screenshotInfo => {
|
|
101
|
-
screenshotCount++;
|
|
102
|
-
this.emit('screenshot-captured', screenshotInfo);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
97
|
const env = {
|
|
106
98
|
...process.env,
|
|
107
99
|
VIZZLY_SERVER_URL: `http://localhost:${this.config.server.port}`,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vizzly Client Type Definitions
|
|
3
|
+
* Lightweight client for test runners
|
|
4
|
+
* @module @vizzly-testing/cli/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Take a screenshot for visual regression testing
|
|
9
|
+
*
|
|
10
|
+
* @param name - Unique name for the screenshot
|
|
11
|
+
* @param imageBuffer - PNG image data as a Buffer, or a file path to an image
|
|
12
|
+
* @param options - Optional configuration
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Basic usage with Buffer
|
|
16
|
+
* import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
|
|
17
|
+
*
|
|
18
|
+
* const screenshot = await page.screenshot();
|
|
19
|
+
* await vizzlyScreenshot('homepage', screenshot);
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Basic usage with file path
|
|
23
|
+
* await vizzlyScreenshot('homepage', './screenshots/homepage.png');
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // With properties and threshold
|
|
27
|
+
* await vizzlyScreenshot('checkout-form', screenshot, {
|
|
28
|
+
* properties: { browser: 'chrome', viewport: '1920x1080' },
|
|
29
|
+
* threshold: 5
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
export function vizzlyScreenshot(
|
|
33
|
+
name: string,
|
|
34
|
+
imageBuffer: Buffer | string,
|
|
35
|
+
options?: {
|
|
36
|
+
properties?: Record<string, unknown>;
|
|
37
|
+
threshold?: number;
|
|
38
|
+
fullPage?: boolean;
|
|
39
|
+
}
|
|
40
|
+
): Promise<void>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wait for all queued screenshots to be processed
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* afterAll(async () => {
|
|
47
|
+
* await vizzlyFlush();
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
export function vizzlyFlush(): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if the Vizzly client is initialized and ready
|
|
54
|
+
*
|
|
55
|
+
* @returns True if client is ready, false otherwise
|
|
56
|
+
*/
|
|
57
|
+
export function isVizzlyReady(): boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configure the client with custom settings
|
|
61
|
+
*
|
|
62
|
+
* @param config - Configuration options
|
|
63
|
+
*/
|
|
64
|
+
export function configure(config?: {
|
|
65
|
+
serverUrl?: string;
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
}): void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Enable or disable screenshot capture
|
|
71
|
+
*
|
|
72
|
+
* @param enabled - Whether to enable screenshots
|
|
73
|
+
*/
|
|
74
|
+
export function setEnabled(enabled: boolean): void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get information about Vizzly client state
|
|
78
|
+
*
|
|
79
|
+
* @returns Client information
|
|
80
|
+
*/
|
|
81
|
+
export function getVizzlyInfo(): {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
serverUrl: string | null;
|
|
84
|
+
ready: boolean;
|
|
85
|
+
buildId: string | null;
|
|
86
|
+
tddMode: boolean;
|
|
87
|
+
disabled: boolean;
|
|
88
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vizzly Config Type Definitions
|
|
3
|
+
* Configuration helpers
|
|
4
|
+
* @module @vizzly-testing/cli/config
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { VizzlyConfig } from './index';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Define Vizzly configuration with type hints
|
|
11
|
+
*
|
|
12
|
+
* Use this in your vizzly.config.js for better IDE support:
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { defineConfig } from '@vizzly-testing/cli/config';
|
|
16
|
+
*
|
|
17
|
+
* export default defineConfig({
|
|
18
|
+
* apiKey: process.env.VIZZLY_TOKEN,
|
|
19
|
+
* server: {
|
|
20
|
+
* port: 47392
|
|
21
|
+
* },
|
|
22
|
+
* comparison: {
|
|
23
|
+
* threshold: 0.1
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export function defineConfig(
|
|
28
|
+
config: import('./index').VizzlyConfig
|
|
29
|
+
): import('./index').VizzlyConfig;
|