vitest-image-snapshot 0.6.15

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 (25) hide show
  1. package/README.md +233 -0
  2. package/dist/index.d.ts +112 -0
  3. package/dist/index.js +498 -0
  4. package/package.json +47 -0
  5. package/src/DiffReport.ts +316 -0
  6. package/src/ImageComparison.ts +119 -0
  7. package/src/ImageSnapshotMatcher.ts +144 -0
  8. package/src/ImageSnapshotReporter.ts +91 -0
  9. package/src/PNGUtil.ts +13 -0
  10. package/src/SnapshotManager.ts +90 -0
  11. package/src/index.ts +8 -0
  12. package/src/reporter.ts +3 -0
  13. package/src/test/ImageComparison.test.ts +98 -0
  14. package/src/test/ReporterE2E.test.ts +40 -0
  15. package/src/test/SnapshotManager.test.ts +55 -0
  16. package/src/test/fixtures/failing-snapshot/__image_actual__/red-square.png +0 -0
  17. package/src/test/fixtures/failing-snapshot/__image_diff_report__/index.html +187 -0
  18. package/src/test/fixtures/failing-snapshot/__image_diff_report__/packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_actual__/red-square.png +0 -0
  19. package/src/test/fixtures/failing-snapshot/__image_diff_report__/packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png +0 -0
  20. package/src/test/fixtures/failing-snapshot/__image_diff_report__/packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png +0 -0
  21. package/src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png +0 -0
  22. package/src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png +0 -0
  23. package/src/test/fixtures/failing-snapshot/red-vs-blue.test.ts +26 -0
  24. package/src/test/fixtures/failing-snapshot/vitest.fixture.config.ts +18 -0
  25. package/src/vitest.d.ts +25 -0
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Image Snapshot Testing
2
+
3
+ Visual regression testing for images. Compare rendered outputs against reference images to catch visual bugs.
4
+
5
+ - Native Vitest integration, TypeScript-first
6
+ - Accepts `ImageData` or PNG buffers - test WebGPU, Canvas, or any image processing
7
+ - Aggregated HTML diff report for visual failures
8
+
9
+
10
+ ## Setup
11
+
12
+ ### 1. Install
13
+
14
+ ```bash
15
+ pnpm install --save-dev vitest-image-snapshot
16
+ ```
17
+
18
+ ### 2. Import in your test file
19
+
20
+ ```typescript
21
+ import { imageMatcher } from "vitest-image-snapshot";
22
+
23
+ imageMatcher(); // Call once at the top level
24
+ ```
25
+
26
+ ### 3. (Optional) Configure HTML diff report
27
+
28
+ Configure the reporter in `vitest.config.ts`:
29
+
30
+ ```typescript
31
+ import { defineConfig } from 'vitest/config'
32
+ import { fileURLToPath } from 'node:url'
33
+ import { dirname, join } from 'node:path'
34
+
35
+ const __dirname = dirname(fileURLToPath(import.meta.url))
36
+
37
+ export default defineConfig({
38
+ test: {
39
+ include: ['src/test/**/*.test.ts'],
40
+ reporters: [
41
+ 'default',
42
+ ['vitest-image-snapshot/reporter', {
43
+ reportPath: join(__dirname, '__image_diff_report__'), // Absolute path recommended for monorepos
44
+ autoOpen: true, // Auto-open report in browser on failure
45
+ }]
46
+ ],
47
+ },
48
+ })
49
+ ```
50
+
51
+ **Default behavior** (no configuration):
52
+ - Report location: `{vitest.config.root}/__image_diff_report__/index.html`
53
+ - Auto-open: `false` (can override with `IMAGE_DIFF_AUTO_OPEN=true` env var)
54
+
55
+ **Configuration options:**
56
+ - `reportPath`: Absolute or relative to `config.root` (default: `'__image_diff_report__'`)
57
+ - `autoOpen`: Auto-open report in browser (default: `false`)
58
+
59
+ ## Basic Usage
60
+
61
+ Accepts standard browser `ImageData` or `Buffer` (pre-encoded PNGs):
62
+
63
+ ```typescript
64
+ // With ImageData (from canvas, shader tests, etc.)
65
+ await expect(imageData).toMatchImage("snapshot-name");
66
+
67
+ // With Buffer (pre-encoded PNG)
68
+ await expect(pngBuffer).toMatchImage("snapshot-name");
69
+
70
+ // Auto-generated name from test name (single snapshot per test only)
71
+ await expect(imageData).toMatchImage();
72
+ ```
73
+
74
+ ### Options
75
+
76
+ ```typescript
77
+ await expect(imageData).toMatchImage({
78
+ name: "edge-detection",
79
+ threshold: 0.2, // Allow more color variation
80
+ });
81
+ ```
82
+
83
+ See [Match Options](#match-options) for all available configuration options.
84
+
85
+ ## Updating Snapshots
86
+
87
+ When you intentionally change shader behavior:
88
+
89
+ ```bash
90
+ # Update all snapshots
91
+ pnpm vitest -- -u
92
+
93
+ # Update specific test file
94
+ pnpm vitest ImageSnapshot.test.ts -- -u
95
+ ```
96
+
97
+ ## Diff Report
98
+
99
+ If you added `ImageSnapshotReporter` to vitest.config.ts, failed tests generate a self-contained HTML report with:
100
+ - Expected vs Actual side-by-side
101
+ - Diff visualization (mismatched pixels highlighted)
102
+ - Mismatch statistics
103
+ - All images copied to report directory (portable and shareable)
104
+
105
+ **Default location**: `{vitest.config.root}/__image_diff_report__/index.html`
106
+
107
+ **Monorepo behavior**: In workspace mode (running tests from workspace root), each package's report goes to its own directory when using absolute paths in config.
108
+
109
+ Auto-open on failure:
110
+ ```bash
111
+ IMAGE_DIFF_AUTO_OPEN=true pnpm vitest
112
+ ```
113
+
114
+ Or enable via inline reporter config:
115
+ ```typescript
116
+ reporters: [
117
+ 'default',
118
+ ['vitest-image-snapshot/reporter', { autoOpen: true }]
119
+ ]
120
+ ```
121
+
122
+ ## Directory Structure
123
+
124
+ ```
125
+ package-root/
126
+ ├── src/test/
127
+ │ ├── ImageSnapshot.test.ts
128
+ │ ├── __image_snapshots__/ # Reference images (commit to git)
129
+ │ │ └── snapshot-name.png
130
+ │ ├── __image_actual__/ # Current test outputs (gitignore, always saved)
131
+ │ │ └── snapshot-name.png
132
+ │ └── __image_diffs__/ # Diff visualizations (gitignore, only on failure)
133
+ │ └── snapshot-name.png
134
+ └── __image_diff_report__/ # HTML report (gitignore, self-contained)
135
+ ├── index.html
136
+ └── src/test/ # Copied images preserving directory structure
137
+ ├── __image_snapshots__/
138
+ ├── __image_actual__/
139
+ └── __image_diffs__/
140
+ ```
141
+
142
+ **Notes**:
143
+ - `__image_actual__/` saves on every run (pass or fail) for manual inspection
144
+ - Report copies all images to `__image_diff_report__/` preserving directory structure
145
+ - Report is self-contained and portable (can be zipped, shared, or committed)
146
+
147
+ ## API Reference
148
+
149
+ ### toMatchImage()
150
+
151
+ Vitest matcher for comparing images against reference snapshots.
152
+
153
+ ```typescript
154
+ await expect(imageData).toMatchImage(nameOrOptions?)
155
+ ```
156
+
157
+ #### Parameters:
158
+ - `imageData: ImageData | Buffer` - Image to compare
159
+ - `nameOrOptions?: string | MatchImageOptions` - Snapshot name or options
160
+
161
+ #### Match Options:
162
+ ```typescript
163
+ interface MatchImageOptions {
164
+ name?: string; // Snapshot name (default: auto-generated from test name)
165
+ threshold?: number; // Color difference threshold 0-1 (default: 0.1)
166
+ allowedPixelRatio?: number; // Max ratio of pixels allowed to differ 0-1 (default: 0)
167
+ allowedPixels?: number; // Max absolute pixels allowed to differ (default: 0)
168
+ includeAA?: boolean; // Disable AA detection if true (default: false)
169
+ }
170
+ ```
171
+
172
+ ## Examples
173
+
174
+ ### WebGPU Shaders
175
+
176
+ ```typescript
177
+ import { imageMatcher } from "vitest-image-snapshot";
178
+ import { testFragmentShaderImage } from "wesl-debug";
179
+
180
+ imageMatcher();
181
+
182
+ test("shader output matches snapshot", async () => {
183
+ const result = await testFragmentShaderImage({
184
+ projectDir: import.meta.url,
185
+ device,
186
+ src: `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1.0, 0.0, 0.0, 1.0); }`,
187
+ size: [128, 128],
188
+ });
189
+
190
+ await expect(result).toMatchImage("red-output");
191
+ });
192
+ ```
193
+
194
+ ### Canvas/DOM ImageData
195
+
196
+ ```typescript
197
+ import { imageMatcher } from "vitest-image-snapshot";
198
+
199
+ imageMatcher();
200
+
201
+ test("canvas output matches snapshot", async () => {
202
+ const canvas = document.createElement('canvas');
203
+ canvas.width = 128;
204
+ canvas.height = 128;
205
+ const ctx = canvas.getContext('2d')!;
206
+
207
+ // Draw something
208
+ ctx.fillStyle = 'red';
209
+ ctx.fillRect(0, 0, 128, 128);
210
+
211
+ const imageData = ctx.getImageData(0, 0, 128, 128);
212
+ await expect(imageData).toMatchImage("red-canvas");
213
+ });
214
+ ```
215
+
216
+ ## Troubleshooting
217
+
218
+ ### "No reference snapshot found"
219
+ **First run**: Snapshot created automatically
220
+ **CI**: Run with `-u` locally first, then commit snapshots
221
+
222
+ ### Images don't match but look identical
223
+ - `threshold` too strict - increase tolerance
224
+ - GPU/driver differences - use `allowedPixelRatio`
225
+ - Anti-aliasing differences - set `includeAA: true`
226
+
227
+ ## Build Version
228
+ **vitest-image-snapshot** is currently
229
+ part of the [wesl-js](https://github.com/wgsl-tooling-wg/wesl-js/) monorepo.
230
+ (It'll eventually move to it's own repo).
231
+
232
+ ## Contributions
233
+ See [Implementation.md](./Implementation.md) for details and feature ideas.
@@ -0,0 +1,112 @@
1
+ import { Reporter, TestCase, Vitest } from "vitest/node";
2
+
3
+ //#region src/ImageComparison.d.ts
4
+ /** nodejs compatible interface to DOM ImageData */
5
+ interface ImageData {
6
+ readonly data: Uint8ClampedArray;
7
+ readonly width: number;
8
+ readonly height: number;
9
+ readonly colorSpace: "srgb" | "display-p3";
10
+ }
11
+ /** Options controlling image comparison thresholds and behavior. */
12
+ interface ComparisonOptions {
13
+ /** Color difference threshold (0-1). Default: 0.1 */
14
+ threshold?: number;
15
+ /** Max ratio of pixels allowed to differ (0-1). Default: 0 */
16
+ allowedPixelRatio?: number;
17
+ /** Max absolute number of pixels allowed to differ. Default: 0 */
18
+ allowedPixels?: number;
19
+ /** If true, disables detecting and ignoring anti-aliased pixels. Default: true */
20
+ includeAA?: boolean;
21
+ }
22
+ interface ComparisonResult {
23
+ pass: boolean;
24
+ diffBuffer?: Buffer;
25
+ message: string;
26
+ mismatchedPixels: number;
27
+ mismatchedPixelRatio: number;
28
+ }
29
+ declare function compareImages(reference: ImageData | Buffer, actual: ImageData | Buffer, options?: ComparisonOptions): Promise<ComparisonResult>;
30
+ //#endregion
31
+ //#region src/DiffReport.d.ts
32
+ /** Failed image snapshot with comparison results and file paths. */
33
+ interface ImageSnapshotFailure {
34
+ testName: string;
35
+ snapshotName: string;
36
+ comparison: ComparisonResult;
37
+ paths: {
38
+ reference: string;
39
+ actual: string;
40
+ diff: string;
41
+ };
42
+ }
43
+ /** Configuration for HTML diff report generation. */
44
+ interface DiffReportConfig {
45
+ /** Auto-open report in browser. Default: false */
46
+ autoOpen?: boolean;
47
+ /** Directory path for generated HTML report (absolute or relative). */
48
+ reportDir: string;
49
+ /** Vitest config root for calculating relative paths when copying images */
50
+ configRoot: string;
51
+ }
52
+ /** Generate HTML diff report for all failed image snapshots. */
53
+ declare function generateDiffReport(failures: ImageSnapshotFailure[], config: DiffReportConfig): Promise<void>;
54
+ //#endregion
55
+ //#region src/ImageSnapshotMatcher.d.ts
56
+ interface MatchImageOptions extends ComparisonOptions {
57
+ name?: string;
58
+ }
59
+ /** Register toMatchImage() matcher with Vitest */
60
+ declare function imageMatcher(): void;
61
+ //#endregion
62
+ //#region src/ImageSnapshotReporter.d.ts
63
+ interface ImageSnapshotReporterOptions {
64
+ /** Report directory (relative to config.root or absolute) */
65
+ reportPath?: string;
66
+ autoOpen?: boolean;
67
+ }
68
+ /** Vitest reporter that generates HTML diff reports for image snapshot failures */
69
+ declare class ImageSnapshotReporter implements Reporter {
70
+ private failures;
71
+ private vitest;
72
+ private reportPath?;
73
+ private autoOpen;
74
+ constructor(options?: ImageSnapshotReporterOptions);
75
+ onInit(vitest: Vitest): void;
76
+ onTestCaseResult(testCase: TestCase): void;
77
+ onTestRunEnd(): Promise<void>;
78
+ private resolveReportDir;
79
+ }
80
+ //#endregion
81
+ //#region src/PNGUtil.d.ts
82
+ /** Convert standard browser ImageData to PNG Buffer for comparison. */
83
+ declare function pngBuffer(imageData: ImageData): Buffer;
84
+ //#endregion
85
+ //#region src/SnapshotManager.d.ts
86
+ interface SnapshotConfig {
87
+ snapshotDir?: string;
88
+ diffDir?: string;
89
+ actualDir?: string;
90
+ /** Vitest snapshot update mode: "all", "new", or "none" */
91
+ updateSnapshot?: string;
92
+ }
93
+ /** Manage reference/actual/diff snapshot files. */
94
+ declare class ImageSnapshotManager {
95
+ private config;
96
+ private testDir;
97
+ constructor(testFilePath: string, config?: SnapshotConfig);
98
+ referencePath(snapshotName: string): string;
99
+ actualPath(snapshotName: string): string;
100
+ diffPath(snapshotName: string): string;
101
+ /** Update failing snapshots (only in "all" mode with vitest -u) */
102
+ shouldUpdate(): boolean;
103
+ /** Create missing snapshots ("all" or "new" mode) */
104
+ shouldCreateNew(): boolean;
105
+ loadReference(snapshotName: string): Promise<Buffer | null>;
106
+ saveReference(buffer: Buffer, snapshotName: string): Promise<void>;
107
+ saveActual(buffer: Buffer, snapshotName: string): Promise<void>;
108
+ saveDiff(buffer: Buffer, snapshotName: string): Promise<void>;
109
+ private saveToPath;
110
+ }
111
+ //#endregion
112
+ export { ComparisonOptions, ComparisonResult, DiffReportConfig, ImageData, ImageSnapshotFailure, ImageSnapshotManager, ImageSnapshotReporter, ImageSnapshotReporterOptions, MatchImageOptions, SnapshotConfig, compareImages, generateDiffReport, imageMatcher, pngBuffer };