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.
- package/README.md +233 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +498 -0
- package/package.json +47 -0
- package/src/DiffReport.ts +316 -0
- package/src/ImageComparison.ts +119 -0
- package/src/ImageSnapshotMatcher.ts +144 -0
- package/src/ImageSnapshotReporter.ts +91 -0
- package/src/PNGUtil.ts +13 -0
- package/src/SnapshotManager.ts +90 -0
- package/src/index.ts +8 -0
- package/src/reporter.ts +3 -0
- package/src/test/ImageComparison.test.ts +98 -0
- package/src/test/ReporterE2E.test.ts +40 -0
- package/src/test/SnapshotManager.test.ts +55 -0
- package/src/test/fixtures/failing-snapshot/__image_actual__/red-square.png +0 -0
- package/src/test/fixtures/failing-snapshot/__image_diff_report__/index.html +187 -0
- 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
- 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
- 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
- package/src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png +0 -0
- package/src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png +0 -0
- package/src/test/fixtures/failing-snapshot/red-vs-blue.test.ts +26 -0
- package/src/test/fixtures/failing-snapshot/vitest.fixture.config.ts +18 -0
- 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.
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|