@vizzly-testing/honeydiff 0.2.0 → 0.2.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/README.md
CHANGED
|
@@ -4,205 +4,300 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
5
|
[](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Blazingly fast image comparison for Node.js
|
|
8
|
-
validation, and detecting visual changes.
|
|
9
|
-
|
|
10
|
-
**Why Honeydiff?**
|
|
11
|
-
- **Fast:** Native performance processing 230M+ pixels/sec
|
|
12
|
-
- **Smart:** Intelligent anti-aliasing detection filters rendering artifacts
|
|
13
|
-
- **Accessible:** Built-in WCAG contrast checking and color blindness simulation
|
|
14
|
-
- **Flexible:** File paths or Buffers, async or sync APIs
|
|
15
|
-
- **Complete:** TypeScript types, pre-built binaries for all platforms
|
|
16
|
-
- **Advanced:** SSIM perceptual scoring, spatial clustering, variable-height screenshots
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
7
|
+
**Blazingly fast image comparison for visual regression testing in Node.js.** Built with Rust for native performance.
|
|
19
8
|
|
|
20
9
|
```bash
|
|
21
10
|
npm install @vizzly-testing/honeydiff
|
|
22
11
|
```
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
Requires Node.js 22+. Pre-built binaries included for macOS (ARM64), Linux (x64/ARM64), and Windows (x64).
|
|
14
|
+
|
|
15
|
+
## Why Honeydiff?
|
|
16
|
+
|
|
17
|
+
- **6x faster than odiff** - Processes 230M+ pixels/sec with multi-core parallelization
|
|
18
|
+
- **Smart anti-aliasing** - Filters out rendering artifacts, not actual changes
|
|
19
|
+
- **Accessibility built-in** - WCAG contrast checking and color blindness simulation
|
|
20
|
+
- **Flexible inputs** - Works with file paths or Buffers
|
|
21
|
+
- **Full TypeScript support** - Complete type definitions included
|
|
22
|
+
- **Advanced analytics** - SSIM perceptual scoring, spatial clustering, intensity stats
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
required.
|
|
24
|
+
Built for production visual regression testing at scale.
|
|
28
25
|
|
|
29
26
|
## Quick Start
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
### Basic Comparison
|
|
29
|
+
|
|
32
30
|
```javascript
|
|
33
31
|
const { compare, quickCompare } = require('@vizzly-testing/honeydiff');
|
|
34
32
|
|
|
35
|
-
// Quick check
|
|
33
|
+
// Quick boolean check - are these images different?
|
|
36
34
|
if (await quickCompare('baseline.png', 'current.png')) {
|
|
37
35
|
console.log('Visual regression detected!');
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
// Detailed comparison
|
|
38
|
+
// Detailed comparison with tolerance
|
|
41
39
|
const result = await compare('baseline.png', 'current.png', {
|
|
42
|
-
pixelTolerance: 10,
|
|
43
|
-
includeSSIM: true
|
|
40
|
+
pixelTolerance: 10, // Ignore tiny differences
|
|
41
|
+
includeSSIM: true // Perceptual similarity score
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
console.log(`Diff: ${result.diffPercentage.toFixed(2)}
|
|
44
|
+
console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
45
|
+
console.log(`SSIM: ${result.perceptualScore}`);
|
|
47
46
|
// Output: Diff: 3.55%, SSIM: 0.9234
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
### TypeScript
|
|
51
50
|
|
|
52
51
|
```typescript
|
|
53
52
|
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
54
53
|
|
|
55
|
-
const
|
|
54
|
+
const options: CompareOptions = {
|
|
56
55
|
pixelTolerance: 5,
|
|
57
56
|
includeSSIM: true,
|
|
58
57
|
includeClusters: true
|
|
59
|
-
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result: DiffResult = await compare('img1.png', 'img2.png', options);
|
|
60
61
|
|
|
61
62
|
if (result.diffClusters) {
|
|
62
|
-
console.log(`Found ${result.diffClusters.length}
|
|
63
|
+
console.log(`Found ${result.diffClusters.length} different regions`);
|
|
63
64
|
}
|
|
64
|
-
// Output: Found 3 diff regions
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
### Test Integration
|
|
68
68
|
|
|
69
|
-
**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- `getDimensions(img): Promise<{ width: number, height: number }>` - Get image dimensions
|
|
69
|
+
**Jest/Vitest**
|
|
70
|
+
```javascript
|
|
71
|
+
import { quickCompare } from '@vizzly-testing/honeydiff';
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
test('homepage should match baseline', async () => {
|
|
74
|
+
let screenshot = await page.screenshot();
|
|
75
|
+
let isDifferent = await quickCompare('baseline.png', screenshot);
|
|
76
|
+
expect(isDifferent).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
```
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
**Playwright**
|
|
81
|
+
```javascript
|
|
82
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
test('button hover state', async () => {
|
|
85
|
+
await page.hover('[data-testid="submit-button"]');
|
|
86
|
+
let screenshot = await page.screenshot();
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let result = await compare('baseline.png', screenshot, {
|
|
89
|
+
pixelTolerance: 10,
|
|
90
|
+
diffPath: './artifacts/diff.png'
|
|
91
|
+
});
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
pixelTolerance?: number; // 0-255 (default: 0)
|
|
89
|
-
colorThreshold?: number; // 0.0-1.0 for YIQ mode (default: 0.0 = RGB)
|
|
90
|
-
antialiasing?: boolean; // Enable AA detection (default: true)
|
|
91
|
-
ignoreColors?: boolean; // Brightness only (default: false)
|
|
92
|
-
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
93
|
-
includeDiffPixels?: boolean; // Detailed pixel list (default: false)
|
|
94
|
-
includeClusters?: boolean; // Spatial clusters (default: false)
|
|
95
|
-
includeSSIM?: boolean; // SSIM score (default: false)
|
|
96
|
-
includeAccessibilityData?: boolean; // RGB, luminance, WCAG (default: false)
|
|
97
|
-
checkColorBlindness?: boolean; // Color blindness simulation (default: false)
|
|
98
|
-
colorBlindnessThreshold?: number; // Visibility threshold 0-255 (default: 30)
|
|
99
|
-
diffPath?: string; // Save diff image
|
|
100
|
-
maskPath?: string; // Save mask image
|
|
101
|
-
overlayPath?: string; // Save overlay image
|
|
102
|
-
overwrite?: boolean; // Overwrite files (default: false)
|
|
103
|
-
}
|
|
93
|
+
expect(result.diffPercentage).toBeLessThan(1.0);
|
|
94
|
+
});
|
|
104
95
|
```
|
|
105
|
-
</details>
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
<summary><strong>DiffResult</strong></summary>
|
|
97
|
+
## Core Features
|
|
109
98
|
|
|
110
|
-
|
|
111
|
-
interface DiffResult {
|
|
112
|
-
isDifferent: boolean;
|
|
113
|
-
diffPercentage: number;
|
|
114
|
-
totalPixels: number;
|
|
115
|
-
diffPixels: number;
|
|
116
|
-
aaPixelsIgnored: number;
|
|
117
|
-
aaPercentage: number;
|
|
118
|
-
boundingBox: BoundingBox | null;
|
|
119
|
-
heightDiff: HeightDiff | null;
|
|
120
|
-
diffPixelsList: DiffPixel[] | null;
|
|
121
|
-
diffClusters: DiffCluster[] | null;
|
|
122
|
-
intensityStats: IntensityStats | null;
|
|
123
|
-
perceptualScore: number | null; // SSIM 0.0-1.0
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
</details>
|
|
99
|
+
### 1. Performance at Scale
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
Process 230M pixels/second with automatic multi-core parallelization. Perfect for CI/CD pipelines.
|
|
129
102
|
|
|
130
|
-
**Get Image Dimensions**
|
|
131
103
|
```javascript
|
|
132
|
-
|
|
104
|
+
// 18.3M pixel screenshot compared in ~79ms
|
|
105
|
+
const result = await compare('full-page-baseline.png', 'full-page-current.png');
|
|
106
|
+
```
|
|
133
107
|
|
|
134
|
-
|
|
135
|
-
let dims = await getDimensions('screenshot.png');
|
|
136
|
-
console.log(`Image is ${dims.width}x${dims.height} pixels`);
|
|
137
|
-
// Output: Image is 1920x1080 pixels
|
|
108
|
+
### 2. Smart Anti-Aliasing Detection
|
|
138
109
|
|
|
139
|
-
|
|
140
|
-
let dims = getDimensionsSync('screenshot.png');
|
|
141
|
-
console.log(`Image is ${dims.width}x${dims.height} pixels`);
|
|
110
|
+
Automatically filters out rendering artifacts (font smoothing, sub-pixel differences) while catching real visual changes.
|
|
142
111
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
112
|
+
```javascript
|
|
113
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
114
|
+
antialiasing: true // Default: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log(`Ignored ${result.aaPixelsIgnored} AA pixels`);
|
|
118
|
+
// Output: Ignored 1952904 AA pixels
|
|
146
119
|
```
|
|
147
120
|
|
|
148
|
-
|
|
121
|
+
Disable for pixel-perfect matching:
|
|
149
122
|
```javascript
|
|
150
|
-
const
|
|
151
|
-
|
|
123
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
124
|
+
antialiasing: false // Strict pixel matching
|
|
125
|
+
});
|
|
126
|
+
```
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
128
|
+
### 3. Variable Height Screenshots
|
|
129
|
+
|
|
130
|
+
Perfect for scrollable content - compare full-page screenshots with different heights.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// 24,162px vs 24,412px tall images
|
|
134
|
+
const result = await compare('full-page-v1.png', 'full-page-v2.png');
|
|
135
|
+
|
|
136
|
+
if (result.heightDiff) {
|
|
137
|
+
console.log(`Height difference: ${result.heightDiff.height1} vs ${result.heightDiff.height2}`);
|
|
138
|
+
console.log(`Extra pixels: ${result.heightDiff.extraPixels}`);
|
|
139
|
+
}
|
|
156
140
|
```
|
|
157
141
|
|
|
158
|
-
|
|
142
|
+
### 4. Visual Diff Artifacts
|
|
143
|
+
|
|
144
|
+
Generate highlighted diff images, binary masks, and overlays for debugging.
|
|
159
145
|
|
|
160
146
|
```javascript
|
|
161
|
-
|
|
162
|
-
diffPath: './diff.png',
|
|
163
|
-
maskPath: './mask.png',
|
|
164
|
-
overlayPath: './overlay.png',
|
|
147
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
148
|
+
diffPath: './artifacts/diff.png', // Highlighted differences
|
|
149
|
+
maskPath: './artifacts/mask.png', // Binary diff mask
|
|
150
|
+
overlayPath: './artifacts/overlay.png', // Side-by-side comparison
|
|
165
151
|
overwrite: true
|
|
166
152
|
});
|
|
153
|
+
|
|
154
|
+
console.log('Artifacts saved to ./artifacts/');
|
|
167
155
|
```
|
|
168
156
|
|
|
169
|
-
|
|
157
|
+
### 5. Spatial Clustering
|
|
158
|
+
|
|
159
|
+
Group nearby differences into regions to identify distinct changed areas.
|
|
160
|
+
|
|
170
161
|
```javascript
|
|
171
|
-
|
|
172
|
-
includeClusters: true
|
|
173
|
-
includeSSIM: true,
|
|
174
|
-
includeDiffPixels: true
|
|
162
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
163
|
+
includeClusters: true
|
|
175
164
|
});
|
|
176
165
|
|
|
177
|
-
// Spatial clusters
|
|
178
166
|
if (result.diffClusters) {
|
|
179
|
-
result.diffClusters.forEach((
|
|
180
|
-
console.log(`Region ${i + 1}
|
|
167
|
+
result.diffClusters.forEach((cluster, i) => {
|
|
168
|
+
console.log(`Region ${i + 1}:`);
|
|
169
|
+
console.log(` - ${cluster.pixelCount} pixels changed`);
|
|
170
|
+
console.log(` - Center at (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
|
|
171
|
+
console.log(` - Bounding box: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
|
|
172
|
+
console.log(` - Average intensity: ${cluster.avgIntensity}`);
|
|
181
173
|
});
|
|
182
174
|
}
|
|
183
175
|
// Output:
|
|
184
|
-
// Region 1:
|
|
185
|
-
//
|
|
186
|
-
//
|
|
176
|
+
// Region 1:
|
|
177
|
+
// - 12450 pixels changed
|
|
178
|
+
// - Center at (245, 189)
|
|
179
|
+
// - Bounding box: 124x89
|
|
180
|
+
// - Average intensity: 127.3
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 6. Perceptual Similarity (SSIM)
|
|
184
|
+
|
|
185
|
+
Beyond pixel counting - measure structural similarity from a human perception perspective.
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
189
|
+
includeSSIM: true // Note: Can be slow on large images
|
|
190
|
+
});
|
|
187
191
|
|
|
188
|
-
// SSIM score
|
|
189
192
|
if (result.perceptualScore !== null) {
|
|
190
193
|
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
194
|
+
// Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
|
|
195
|
+
|
|
196
|
+
if (result.perceptualScore > 0.95) {
|
|
197
|
+
console.log('Images are perceptually very similar');
|
|
198
|
+
}
|
|
191
199
|
}
|
|
192
|
-
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 7. Tolerance & Color Spaces
|
|
203
|
+
|
|
204
|
+
**RGB Mode (default)** - Exact matching with pixel tolerance:
|
|
205
|
+
```javascript
|
|
206
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
207
|
+
pixelTolerance: 10 // Ignore differences below threshold (0-255)
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**YIQ Mode** - Perceptual color matching weighted for human vision:
|
|
212
|
+
```javascript
|
|
213
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
214
|
+
colorThreshold: 0.1 // YIQ mode (odiff-compatible)
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Brightness-only Mode** - Ignore color, compare structure:
|
|
219
|
+
```javascript
|
|
220
|
+
const result = await compare('img1.png', 'img2.png', {
|
|
221
|
+
ignoreColors: true // Compare brightness/luminance only
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 8. Buffer Support
|
|
226
|
+
|
|
227
|
+
Work with in-memory images from screenshots or API responses.
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
const fs = require('fs');
|
|
231
|
+
|
|
232
|
+
// Compare buffers directly
|
|
233
|
+
let img1 = fs.readFileSync('image1.png');
|
|
234
|
+
let img2 = fs.readFileSync('image2.png');
|
|
235
|
+
let result = await compare(img1, img2, { pixelTolerance: 5 });
|
|
236
|
+
|
|
237
|
+
// Mix paths and buffers
|
|
238
|
+
let screenshot = await page.screenshot(); // Returns Buffer
|
|
239
|
+
let result = await compare('baseline.png', screenshot);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 9. Image Dimensions
|
|
243
|
+
|
|
244
|
+
Fast utility to get image dimensions without loading full image data.
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const { getDimensions } = require('@vizzly-testing/honeydiff');
|
|
248
|
+
|
|
249
|
+
let dims = await getDimensions('screenshot.png');
|
|
250
|
+
console.log(`Image is ${dims.width}x${dims.height} pixels`);
|
|
251
|
+
// Output: Image is 1920x1080 pixels
|
|
252
|
+
|
|
253
|
+
// Works with buffers too
|
|
254
|
+
let buffer = fs.readFileSync('image.png');
|
|
255
|
+
let dims = await getDimensions(buffer);
|
|
256
|
+
```
|
|
193
257
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
258
|
+
## Accessibility Features
|
|
259
|
+
|
|
260
|
+
Built-in accessibility testing to catch WCAG compliance issues and color blindness visibility problems.
|
|
261
|
+
|
|
262
|
+
### WCAG Contrast Checking
|
|
263
|
+
|
|
264
|
+
Validate that visual changes meet WCAG AA/AAA contrast standards.
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
268
|
+
includeClusters: true,
|
|
269
|
+
includeAccessibilityData: true
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (result.diffClusters) {
|
|
273
|
+
result.diffClusters.forEach(cluster => {
|
|
274
|
+
if (cluster.accessibilityMetadata) {
|
|
275
|
+
let meta = cluster.accessibilityMetadata;
|
|
276
|
+
|
|
277
|
+
console.log(`Contrast ratio: ${meta.contrastRatio.toFixed(2)}:1`);
|
|
278
|
+
console.log(`Range: ${meta.minContrastRatio.toFixed(2)} - ${meta.maxContrastRatio.toFixed(2)}`);
|
|
279
|
+
|
|
280
|
+
// WCAG validation
|
|
281
|
+
if (meta.minContrastRatio < 4.5) {
|
|
282
|
+
console.log('⚠️ Below WCAG AA normal text (4.5:1)');
|
|
283
|
+
}
|
|
284
|
+
if (meta.minContrastRatio < 3.0) {
|
|
285
|
+
console.log('⚠️ Below WCAG AA large text (3.0:1)');
|
|
286
|
+
}
|
|
287
|
+
if (meta.minContrastRatio >= 7.0) {
|
|
288
|
+
console.log('✓ Meets WCAG AAA (7.0:1)');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
197
292
|
}
|
|
198
|
-
// Output: Range: 12-255, Mean: 87.3
|
|
199
293
|
```
|
|
200
294
|
|
|
201
|
-
|
|
295
|
+
### RGB Channel Analysis
|
|
296
|
+
|
|
297
|
+
Track which color channels changed - useful for debugging design changes.
|
|
202
298
|
|
|
203
299
|
```javascript
|
|
204
|
-
|
|
205
|
-
let result = await compare('baseline.png', 'current.png', {
|
|
300
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
206
301
|
includeClusters: true,
|
|
207
302
|
includeAccessibilityData: true
|
|
208
303
|
});
|
|
@@ -211,31 +306,40 @@ if (result.diffClusters) {
|
|
|
211
306
|
result.diffClusters.forEach(cluster => {
|
|
212
307
|
if (cluster.accessibilityMetadata) {
|
|
213
308
|
let meta = cluster.accessibilityMetadata;
|
|
309
|
+
|
|
214
310
|
console.log(`RGB diffs: R=${meta.avgRDiff}, G=${meta.avgGDiff}, B=${meta.avgBDiff}`);
|
|
215
311
|
console.log(`Dominant channel: ${meta.dominantChannel}`);
|
|
216
|
-
console.log(`
|
|
217
|
-
|
|
218
|
-
if (meta.contrastRatio < 4.5) {
|
|
219
|
-
console.log('⚠️ Below WCAG AA threshold');
|
|
220
|
-
}
|
|
312
|
+
console.log(`Luminance: baseline=${meta.avgBaselineLuminance.toFixed(3)}, current=${meta.avgCurrentLuminance.toFixed(3)}`);
|
|
221
313
|
}
|
|
222
314
|
});
|
|
223
315
|
}
|
|
316
|
+
// Output:
|
|
317
|
+
// RGB diffs: R=45.2, G=12.3, B=8.7
|
|
318
|
+
// Dominant channel: R
|
|
319
|
+
// Luminance: baseline=0.234, current=0.567
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Color Blindness Simulation
|
|
323
|
+
|
|
324
|
+
Test if visual changes are visible to users with color vision deficiencies.
|
|
224
325
|
|
|
225
|
-
|
|
226
|
-
|
|
326
|
+
```javascript
|
|
327
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
227
328
|
includeClusters: true,
|
|
228
|
-
checkColorBlindness: true
|
|
329
|
+
checkColorBlindness: true,
|
|
330
|
+
colorBlindnessThreshold: 30.0 // Sensitivity threshold (0-255)
|
|
229
331
|
});
|
|
230
332
|
|
|
231
333
|
if (result.diffClusters) {
|
|
232
334
|
result.diffClusters.forEach(cluster => {
|
|
233
335
|
if (cluster.accessibilityMetadata?.colorBlindnessImpact) {
|
|
234
336
|
let impact = cluster.accessibilityMetadata.colorBlindnessImpact;
|
|
337
|
+
|
|
235
338
|
console.log(`Visibility score: ${(impact.visibilityScore * 100).toFixed(0)}%`);
|
|
236
|
-
console.log(`Deuteranopia (red-green): ${impact.deuteranopiaVisible ? '✓' : '✗'}`);
|
|
237
|
-
console.log(`Protanopia (red-weak): ${impact.protanopiaVisible ? '✓' : '✗'}`);
|
|
238
|
-
console.log(`Tritanopia (blue-yellow): ${impact.tritanopiaVisible ? '✓' : '✗'}`);
|
|
339
|
+
console.log(`Deuteranopia (red-green, ~1% of population): ${impact.deuteranopiaVisible ? '✓' : '✗'}`);
|
|
340
|
+
console.log(`Protanopia (red-weak, ~0.5%): ${impact.protanopiaVisible ? '✓' : '✗'}`);
|
|
341
|
+
console.log(`Tritanopia (blue-yellow, rare): ${impact.tritanopiaVisible ? '✓' : '✗'}`);
|
|
342
|
+
console.log(`Achromatopsia (grayscale, very rare): ${impact.achromatopsiaVisible ? '✓' : '✗'}`);
|
|
239
343
|
|
|
240
344
|
if (impact.visibilityScore < 0.5) {
|
|
241
345
|
console.log('⚠️ May not be visible to colorblind users');
|
|
@@ -245,117 +349,229 @@ if (result.diffClusters) {
|
|
|
245
349
|
}
|
|
246
350
|
```
|
|
247
351
|
|
|
248
|
-
**
|
|
352
|
+
**Real-world example:**
|
|
353
|
+
|
|
249
354
|
```javascript
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
355
|
+
test('button color change is accessible', async () => {
|
|
356
|
+
let result = await compare('button-baseline.png', 'button-hover.png', {
|
|
357
|
+
includeClusters: true,
|
|
358
|
+
checkColorBlindness: true
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Ensure button hover is visible to colorblind users
|
|
362
|
+
let lowestVisibility = Math.min(
|
|
363
|
+
...result.diffClusters.map(c =>
|
|
364
|
+
c.accessibilityMetadata?.colorBlindnessImpact?.visibilityScore ?? 1
|
|
365
|
+
)
|
|
366
|
+
);
|
|
254
367
|
|
|
255
|
-
|
|
368
|
+
expect(lowestVisibility).toBeGreaterThan(0.7);
|
|
369
|
+
});
|
|
256
370
|
```
|
|
257
371
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
372
|
+
## Complete API Reference
|
|
373
|
+
|
|
374
|
+
### Async Functions (Recommended)
|
|
375
|
+
|
|
376
|
+
**`compare(img1, img2, options?): Promise<DiffResult>`**
|
|
377
|
+
|
|
378
|
+
Full comparison with detailed results.
|
|
379
|
+
|
|
380
|
+
**`quickCompare(img1, img2): Promise<boolean>`**
|
|
381
|
+
|
|
382
|
+
Fast boolean check - returns true if different, false if identical.
|
|
383
|
+
|
|
384
|
+
**`getDimensions(img): Promise<{ width: number, height: number }>`**
|
|
385
|
+
|
|
386
|
+
Get image dimensions without loading full image data.
|
|
387
|
+
|
|
388
|
+
### Sync Functions (Blocks Event Loop)
|
|
389
|
+
|
|
390
|
+
**`compareSync(img1, img2, options?): DiffResult`**
|
|
391
|
+
|
|
392
|
+
Synchronous comparison. Use only when blocking behavior is required.
|
|
393
|
+
|
|
394
|
+
**`quickCompareSync(img1, img2): boolean`**
|
|
395
|
+
|
|
396
|
+
Synchronous boolean check.
|
|
397
|
+
|
|
398
|
+
**`getDimensionsSync(img): { width: number, height: number }`**
|
|
399
|
+
|
|
400
|
+
Synchronous dimension retrieval.
|
|
401
|
+
|
|
402
|
+
### Input Types
|
|
403
|
+
|
|
404
|
+
All functions accept:
|
|
405
|
+
- `string` - File path to PNG image
|
|
406
|
+
- `Buffer` - Image data in memory
|
|
407
|
+
|
|
408
|
+
### CompareOptions
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
interface CompareOptions {
|
|
412
|
+
// Basic options
|
|
413
|
+
pixelTolerance?: number; // 0-255, ignore diffs below threshold (default: 0)
|
|
414
|
+
colorThreshold?: number; // 0.0-1.0, YIQ mode threshold (default: 0.0 = RGB)
|
|
415
|
+
antialiasing?: boolean; // Ignore AA artifacts (default: true)
|
|
416
|
+
ignoreColors?: boolean; // Brightness only (default: false)
|
|
417
|
+
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
418
|
+
|
|
419
|
+
// Analysis options
|
|
420
|
+
includeDiffPixels?: boolean; // List all diff pixels (memory intensive, default: false)
|
|
421
|
+
includeClusters?: boolean; // Spatial clustering (default: false)
|
|
422
|
+
includeSSIM?: boolean; // SSIM perceptual score (slow, default: false)
|
|
423
|
+
|
|
424
|
+
// Accessibility options
|
|
425
|
+
includeAccessibilityData?: boolean; // RGB, luminance, WCAG (default: false)
|
|
426
|
+
checkColorBlindness?: boolean; // Color blindness simulation (default: false)
|
|
427
|
+
colorBlindnessThreshold?: number; // Visibility threshold 0-255 (default: 30.0)
|
|
428
|
+
|
|
429
|
+
// Output options
|
|
430
|
+
diffPath?: string; // Save diff image path
|
|
431
|
+
maskPath?: string; // Save mask image path
|
|
432
|
+
overlayPath?: string; // Save overlay image path
|
|
433
|
+
overwrite?: boolean; // Overwrite existing files (default: false)
|
|
299
434
|
}
|
|
300
435
|
```
|
|
301
|
-
</details>
|
|
302
|
-
```
|
|
303
436
|
|
|
304
|
-
|
|
437
|
+
### DiffResult
|
|
305
438
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
439
|
+
```typescript
|
|
440
|
+
interface DiffResult {
|
|
441
|
+
// Basic metrics
|
|
442
|
+
isDifferent: boolean; // Are images different?
|
|
443
|
+
diffPercentage: number; // 0.0-100.0
|
|
444
|
+
totalPixels: number;
|
|
445
|
+
diffPixels: number;
|
|
446
|
+
|
|
447
|
+
// Anti-aliasing
|
|
448
|
+
aaPixelsIgnored: number;
|
|
449
|
+
aaPercentage: number; // 0.0-100.0
|
|
450
|
+
|
|
451
|
+
// Spatial info
|
|
452
|
+
boundingBox: BoundingBox | null; // Rectangle containing all diffs
|
|
453
|
+
heightDiff: HeightDiff | null; // Height difference info
|
|
454
|
+
|
|
455
|
+
// Advanced analysis
|
|
456
|
+
diffPixelsList: DiffPixel[] | null; // Null unless includeDiffPixels enabled
|
|
457
|
+
diffClusters: DiffCluster[] | null; // Null unless includeClusters enabled
|
|
458
|
+
intensityStats: IntensityStats | null; // Null unless includeDiffPixels enabled
|
|
459
|
+
perceptualScore: number | null; // 0.0-1.0, null unless includeSSIM enabled
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
See [index.d.ts](./index.d.ts) for complete type definitions.
|
|
312
464
|
|
|
313
465
|
## Performance
|
|
314
466
|
|
|
315
|
-
|
|
467
|
+
Benchmarked on 18.3M pixel screenshots (750 x 24,162 px):
|
|
316
468
|
|
|
317
469
|
| Operation | Time | Throughput |
|
|
318
470
|
|-----------|------|------------|
|
|
319
|
-
| Basic comparison | ~79ms | 231.8M pixels/sec |
|
|
471
|
+
| Basic RGB comparison | ~79ms | 231.8M pixels/sec |
|
|
472
|
+
| With anti-aliasing | ~88ms | 208M pixels/sec |
|
|
473
|
+
| YIQ color space | ~89ms | 206M pixels/sec |
|
|
320
474
|
| With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
|
|
321
475
|
|
|
322
|
-
**
|
|
476
|
+
**vs odiff:** 6.0x faster (79ms vs 474ms)
|
|
477
|
+
|
|
478
|
+
Multi-core parallelization: 215-309% CPU utilization across cores.
|
|
323
479
|
|
|
324
480
|
*Benchmarked on Apple M-series Mac.*
|
|
325
481
|
|
|
326
|
-
##
|
|
482
|
+
## Common Patterns
|
|
483
|
+
|
|
484
|
+
### CI/CD Integration
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
const { compare } = require('@vizzly-testing/honeydiff');
|
|
327
488
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
489
|
+
async function visualRegressionTest() {
|
|
490
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
491
|
+
pixelTolerance: 10,
|
|
492
|
+
maxDiffs: 1000, // Early exit for fast failure
|
|
493
|
+
diffPath: process.env.CI ? './artifacts/diff.png' : undefined
|
|
494
|
+
});
|
|
334
495
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
496
|
+
if (result.isDifferent && result.diffPercentage > 1.0) {
|
|
497
|
+
console.error(`Visual regression: ${result.diffPercentage.toFixed(2)}% changed`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
338
502
|
|
|
339
|
-
|
|
340
|
-
- RGB mode (default): Exact matching, fastest performance
|
|
341
|
-
- YIQ mode: Perceptual matching weighted for human vision
|
|
503
|
+
### Progressive Enhancement
|
|
342
504
|
|
|
343
|
-
|
|
344
|
-
|
|
505
|
+
```javascript
|
|
506
|
+
// Start with quick check
|
|
507
|
+
if (await quickCompare('baseline.png', 'current.png')) {
|
|
508
|
+
// Only run expensive analysis if different
|
|
509
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
510
|
+
includeClusters: true,
|
|
511
|
+
includeSSIM: true,
|
|
512
|
+
diffPath: './diff.png'
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Detailed analysis and reporting
|
|
516
|
+
reportVisualRegression(result);
|
|
517
|
+
}
|
|
518
|
+
```
|
|
345
519
|
|
|
346
|
-
|
|
347
|
-
|
|
520
|
+
### Tolerance Thresholds
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const result = await compare('baseline.png', 'current.png', {
|
|
524
|
+
pixelTolerance: 10
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
if (result.diffPercentage > 5.0) {
|
|
528
|
+
console.error('FAIL: Major visual regression');
|
|
529
|
+
process.exit(1);
|
|
530
|
+
} else if (result.diffPercentage > 1.0) {
|
|
531
|
+
console.warn('WARN: Minor visual changes detected');
|
|
532
|
+
} else {
|
|
533
|
+
console.log('PASS: Visual test passed');
|
|
534
|
+
}
|
|
535
|
+
```
|
|
348
536
|
|
|
349
537
|
## Troubleshooting
|
|
350
538
|
|
|
351
539
|
**Module not found errors**
|
|
352
|
-
Ensure
|
|
540
|
+
- Ensure Node.js 22+ is installed
|
|
541
|
+
- Verify your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64)
|
|
542
|
+
- Try `npm install` again to download pre-built binaries
|
|
353
543
|
|
|
354
544
|
**Large memory usage with `includeDiffPixels`**
|
|
355
|
-
This option stores every different pixel's position and intensity
|
|
545
|
+
- This option stores every different pixel's position and intensity
|
|
546
|
+
- Disable for large images with many differences
|
|
547
|
+
- Only enable if you need detailed pixel-level data
|
|
356
548
|
|
|
357
549
|
**Slow performance with `includeSSIM`**
|
|
358
|
-
SSIM calculation is O(width × height × window²)
|
|
550
|
+
- SSIM calculation is O(width × height × window²)
|
|
551
|
+
- Only enable when perceptual scoring is needed
|
|
552
|
+
- Consider running SSIM only on failed comparisons
|
|
553
|
+
|
|
554
|
+
**Anti-aliasing too aggressive**
|
|
555
|
+
- Set `antialiasing: false` for stricter pixel matching
|
|
556
|
+
- Adjust `pixelTolerance` instead for controlled flexibility
|
|
557
|
+
|
|
558
|
+
## Use Cases
|
|
559
|
+
|
|
560
|
+
- **Visual Regression Testing** - Catch unintended UI changes in CI/CD
|
|
561
|
+
- **Screenshot Validation** - Compare generated screenshots against baselines
|
|
562
|
+
- **Accessibility Testing** - Validate WCAG contrast and color blindness visibility
|
|
563
|
+
- **Design QA** - Ensure implementations match design specifications
|
|
564
|
+
- **Cross-browser Testing** - Detect rendering differences across browsers
|
|
565
|
+
- **A/B Testing** - Measure visual differences between variants
|
|
566
|
+
- **Responsive Design** - Compare layouts across viewports
|
|
567
|
+
|
|
568
|
+
## Examples
|
|
569
|
+
|
|
570
|
+
See [examples/](./examples/) directory:
|
|
571
|
+
- [basic.js](./examples/basic.js) - Core features and accessibility
|
|
572
|
+
- [buffers.js](./examples/buffers.js) - Working with in-memory images
|
|
573
|
+
- [typescript.ts](./examples/typescript.ts) - TypeScript integration
|
|
574
|
+
- [accessibility.js](./examples/accessibility.js) - WCAG and color blindness
|
|
359
575
|
|
|
360
576
|
## License
|
|
361
577
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/honeydiff",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "High-performance image diffing for Node.js - Native bindings to Honeydiff Rust library",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
"index.d.ts",
|
|
66
66
|
"load-platform.js",
|
|
67
67
|
"platforms/*.node",
|
|
68
|
+
"README.md",
|
|
69
|
+
"index.js",
|
|
70
|
+
"index.d.ts",
|
|
71
|
+
"load-platform.js",
|
|
72
|
+
"platforms/*.node",
|
|
68
73
|
"README.md"
|
|
69
74
|
],
|
|
70
75
|
"engines": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|