vitest-image-snapshot 0.6.22 → 0.6.23

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.
@@ -0,0 +1,22 @@
1
+ import { Reporter, TestCase, Vitest } from "vitest/node";
2
+
3
+ //#region src/ImageSnapshotReporter.d.ts
4
+ interface ImageSnapshotReporterOptions {
5
+ /** Report directory (relative to config.root or absolute) */
6
+ reportPath?: string;
7
+ autoOpen?: boolean;
8
+ }
9
+ /** Vitest reporter that generates HTML diff reports for image snapshot failures */
10
+ declare class ImageSnapshotReporter implements Reporter {
11
+ private failures;
12
+ private vitest;
13
+ private reportPath?;
14
+ private autoOpen;
15
+ constructor(options?: ImageSnapshotReporterOptions);
16
+ onInit(vitest: Vitest): void;
17
+ onTestCaseResult(testCase: TestCase): void;
18
+ onTestRunEnd(): Promise<void>;
19
+ private resolveReportDir;
20
+ }
21
+ //#endregion
22
+ export { ImageSnapshotReporterOptions as n, ImageSnapshotReporter as t };
@@ -0,0 +1,314 @@
1
+ import { exec } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ //#region src/DiffReport.ts
6
+ /** Generate HTML diff report for all failed image snapshots. */
7
+ async function generateDiffReport(failures, config) {
8
+ const { autoOpen = false, reportDir, configRoot } = config;
9
+ if (failures.length === 0) return;
10
+ await fs.promises.mkdir(reportDir, { recursive: true });
11
+ const html = createReportHTML(await copyImagesToReport(failures, reportDir, configRoot));
12
+ const outputPath = path.join(reportDir, "index.html");
13
+ await fs.promises.writeFile(outputPath, html, "utf-8");
14
+ console.log(`\n📊 Image diff report: ${outputPath}`);
15
+ if (autoOpen) exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} "${outputPath}"`);
16
+ }
17
+ /** Copy images to report dir, preserving directory structure relative to configRoot */
18
+ async function copyImagesToReport(failures, reportDir, configRoot) {
19
+ return Promise.all(failures.map(async (failure) => {
20
+ const copiedPaths = await copyImageSet(failure.paths, reportDir, configRoot);
21
+ return {
22
+ ...failure,
23
+ paths: copiedPaths
24
+ };
25
+ }));
26
+ }
27
+ async function copyImageSet(paths, reportDir, configRoot) {
28
+ return {
29
+ reference: await copyImage(paths.reference, reportDir, configRoot),
30
+ actual: await copyImage(paths.actual, reportDir, configRoot),
31
+ diff: await copyImage(paths.diff, reportDir, configRoot)
32
+ };
33
+ }
34
+ /** Copy single image to report dir, preserving directory structure relative to configRoot */
35
+ async function copyImage(sourcePath, reportDir, configRoot) {
36
+ if (!fs.existsSync(sourcePath)) return "";
37
+ const relativePath = path.relative(configRoot, sourcePath);
38
+ const destPath = path.join(reportDir, relativePath);
39
+ const destDir = path.dirname(destPath);
40
+ await fs.promises.mkdir(destDir, { recursive: true });
41
+ await fs.promises.copyFile(sourcePath, destPath);
42
+ return relativePath;
43
+ }
44
+ function createReportHTML(failures) {
45
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleString();
46
+ const totalFailures = failures.length;
47
+ const rows = failures.map((failure) => {
48
+ const { testName, snapshotName, comparison, paths } = failure;
49
+ const { mismatchedPixels, mismatchedPixelRatio } = comparison;
50
+ return `
51
+ <tr>
52
+ <td class="test-name">
53
+ <strong>${escapeHtml(testName)}</strong><br>
54
+ <code>${escapeHtml(snapshotName)}</code>
55
+ </td>
56
+ <td class="image-cell">
57
+ <a href="${paths.reference}" target="_blank">
58
+ <img src="${paths.reference}" alt="Expected" />
59
+ </a>
60
+ <div class="label">Expected</div>
61
+ </td>
62
+ <td class="image-cell">
63
+ <a href="${paths.actual}" target="_blank">
64
+ <img src="${paths.actual}" alt="Actual" />
65
+ </a>
66
+ <div class="label">Actual</div>
67
+ </td>
68
+ <td class="image-cell">
69
+ ${diffCellHTML(paths.diff)}
70
+ </td>
71
+ <td class="stats">
72
+ <div><strong>${mismatchedPixels}</strong> pixels</div>
73
+ <div><strong>${(mismatchedPixelRatio * 100).toFixed(2)}%</strong></div>
74
+ </td>
75
+ </tr>
76
+ `;
77
+ }).join("\n");
78
+ return `<!DOCTYPE html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="UTF-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
+ <title>Image Snapshot Failures - ${totalFailures} failed</title>
84
+ <style>
85
+ * { margin: 0; padding: 0; box-sizing: border-box; }
86
+ body {
87
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
88
+ padding: 20px;
89
+ background: #f5f5f5;
90
+ }
91
+ header {
92
+ background: white;
93
+ padding: 20px;
94
+ border-radius: 8px;
95
+ margin-bottom: 20px;
96
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
97
+ }
98
+ h1 {
99
+ color: #d73027;
100
+ margin-bottom: 10px;
101
+ }
102
+ .meta {
103
+ color: #666;
104
+ font-size: 14px;
105
+ }
106
+ .update-hint {
107
+ background: #fff3cd;
108
+ border: 1px solid #ffc107;
109
+ border-radius: 4px;
110
+ padding: 12px;
111
+ margin: 15px 0;
112
+ }
113
+ .update-hint code {
114
+ background: #f8f9fa;
115
+ padding: 2px 6px;
116
+ border-radius: 3px;
117
+ font-family: 'Monaco', 'Courier New', monospace;
118
+ }
119
+ table {
120
+ width: 100%;
121
+ border-collapse: collapse;
122
+ background: white;
123
+ border-radius: 8px;
124
+ overflow: hidden;
125
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
126
+ }
127
+ th, td {
128
+ padding: 15px;
129
+ text-align: left;
130
+ border-bottom: 1px solid #e0e0e0;
131
+ }
132
+ th {
133
+ background: #f8f9fa;
134
+ font-weight: 600;
135
+ color: #333;
136
+ text-align: center;
137
+ }
138
+ tr:last-child td {
139
+ border-bottom: none;
140
+ }
141
+ tr:hover {
142
+ background: #fafafa;
143
+ }
144
+ .test-name {
145
+ min-width: 200px;
146
+ vertical-align: top;
147
+ }
148
+ .test-name strong {
149
+ color: #333;
150
+ }
151
+ .test-name code {
152
+ color: #666;
153
+ font-size: 12px;
154
+ background: #f5f5f5;
155
+ padding: 2px 6px;
156
+ border-radius: 3px;
157
+ }
158
+ .image-cell {
159
+ text-align: center;
160
+ vertical-align: top;
161
+ width: 25%;
162
+ }
163
+ .image-cell a {
164
+ display: block;
165
+ text-decoration: none;
166
+ }
167
+ .image-cell img {
168
+ max-width: 100%;
169
+ height: auto;
170
+ max-height: 300px;
171
+ border: 1px solid #ddd;
172
+ border-radius: 4px;
173
+ cursor: pointer;
174
+ transition: transform 0.2s;
175
+ }
176
+ .image-cell img:hover {
177
+ transform: scale(1.02);
178
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
179
+ }
180
+ .label {
181
+ margin-top: 8px;
182
+ font-size: 12px;
183
+ color: #666;
184
+ font-weight: 500;
185
+ }
186
+ .no-diff {
187
+ color: #999;
188
+ font-size: 13px;
189
+ font-style: italic;
190
+ padding: 20px;
191
+ text-align: center;
192
+ }
193
+ .stats {
194
+ text-align: center;
195
+ color: #d73027;
196
+ font-size: 14px;
197
+ vertical-align: top;
198
+ }
199
+ .stats div {
200
+ margin: 5px 0;
201
+ }
202
+ footer {
203
+ margin-top: 20px;
204
+ text-align: center;
205
+ color: #999;
206
+ font-size: 12px;
207
+ }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <header>
212
+ <h1>🔴 Image Snapshot Failures</h1>
213
+ <div class="meta">
214
+ <strong>${totalFailures}</strong> test${totalFailures === 1 ? "" : "s"} failed •
215
+ Generated: ${escapeHtml(timestamp)}
216
+ </div>
217
+ <div class="update-hint">
218
+ 💡 <strong>To update snapshots:</strong> Run <code>pnpm test -- -u</code> or <code>vitest -u</code>
219
+ </div>
220
+ </header>
221
+
222
+ <table>
223
+ <thead>
224
+ <tr>
225
+ <th>Test</th>
226
+ <th>Expected</th>
227
+ <th>Actual</th>
228
+ <th>Diff</th>
229
+ <th>Mismatch</th>
230
+ </tr>
231
+ </thead>
232
+ <tbody>
233
+ ${rows}
234
+ </tbody>
235
+ </table>
236
+
237
+ <footer>
238
+ Click images to view full size •
239
+ Diff images highlight mismatched pixels in yellow/red
240
+ </footer>
241
+ </body>
242
+ </html>`;
243
+ }
244
+ function escapeHtml(text) {
245
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
246
+ }
247
+ function diffCellHTML(diffPath) {
248
+ if (!diffPath) return `<div class="no-diff">No diff image<br/>(dimension mismatch)</div>`;
249
+ return `<a href="${diffPath}" target="_blank">
250
+ <img src="${diffPath}" alt="Diff" />
251
+ </a>
252
+ <div class="label">Diff</div>`;
253
+ }
254
+
255
+ //#endregion
256
+ //#region src/ImageSnapshotReporter.ts
257
+ /** Vitest reporter that generates HTML diff reports for image snapshot failures */
258
+ var ImageSnapshotReporter = class {
259
+ failures = [];
260
+ vitest;
261
+ reportPath;
262
+ autoOpen;
263
+ constructor(options = {}) {
264
+ this.reportPath = options.reportPath;
265
+ this.autoOpen = options.autoOpen ?? process.env.IMAGE_DIFF_AUTO_OPEN === "true";
266
+ }
267
+ onInit(vitest) {
268
+ this.vitest = vitest;
269
+ }
270
+ onTestCaseResult(testCase) {
271
+ const result = testCase.result();
272
+ if (result?.state !== "failed") return;
273
+ const meta = testCase.meta();
274
+ if (!meta.imageSnapshotFailure) return;
275
+ const error = result.errors?.[0];
276
+ const failure = captureFailure(testCase, meta.imageSnapshotFailure, error?.message || "");
277
+ this.failures.push(failure);
278
+ }
279
+ async onTestRunEnd() {
280
+ if (this.failures.length === 0) return;
281
+ const reportDir = this.resolveReportDir();
282
+ await generateDiffReport(this.failures, {
283
+ autoOpen: this.autoOpen,
284
+ reportDir,
285
+ configRoot: this.vitest.config.root
286
+ });
287
+ }
288
+ resolveReportDir() {
289
+ const configRoot = this.vitest.config.root;
290
+ if (!this.reportPath) return path.join(configRoot, "__image_diff_report__");
291
+ return path.isAbsolute(this.reportPath) ? this.reportPath : path.join(configRoot, this.reportPath);
292
+ }
293
+ };
294
+ function captureFailure(testCase, data, message) {
295
+ const snapshotName = data.actualPath.match(/([^/]+)\.png$/)?.[1] || "unknown";
296
+ return {
297
+ testName: testCase.fullName || testCase.name,
298
+ snapshotName,
299
+ comparison: {
300
+ pass: false,
301
+ message,
302
+ mismatchedPixels: data.mismatchedPixels,
303
+ mismatchedPixelRatio: data.mismatchedPixelRatio
304
+ },
305
+ paths: {
306
+ reference: data.expectedPath,
307
+ actual: data.actualPath,
308
+ diff: data.diffPath
309
+ }
310
+ };
311
+ }
312
+
313
+ //#endregion
314
+ export { generateDiffReport as n, ImageSnapshotReporter as t };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Reporter, TestCase, Vitest } from "vitest/node";
1
+ import { n as ImageSnapshotReporterOptions, t as ImageSnapshotReporter } from "./ImageSnapshotReporter-C3C7yfn1.js";
2
2
 
3
3
  //#region src/ImageComparison.d.ts
4
4
  /** nodejs compatible interface to DOM ImageData */
@@ -59,25 +59,6 @@ interface MatchImageOptions extends ComparisonOptions {
59
59
  /** Register toMatchImage() matcher with Vitest */
60
60
  declare function imageMatcher(): void;
61
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
62
  //#region src/PNGUtil.d.ts
82
63
  /** Convert standard browser ImageData to PNG Buffer for comparison. */
83
64
  declare function pngBuffer(imageData: ImageData): Buffer;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { exec } from "node:child_process";
1
+ import { n as generateDiffReport, t as ImageSnapshotReporter } from "./ImageSnapshotReporter-D1-9tyxx.js";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import pixelmatch from "pixelmatch";
@@ -6,244 +6,6 @@ import { PNG } from "pngjs";
6
6
  import { expect } from "vitest";
7
7
  import { getCurrentTest } from "vitest/suite";
8
8
 
9
- //#region src/DiffReport.ts
10
- /** Generate HTML diff report for all failed image snapshots. */
11
- async function generateDiffReport(failures, config) {
12
- const { autoOpen = false, reportDir, configRoot } = config;
13
- if (failures.length === 0) return;
14
- if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
15
- const html = createReportHTML(await copyImagesToReport(failures, reportDir, configRoot));
16
- const outputPath = path.join(reportDir, "index.html");
17
- await fs.promises.writeFile(outputPath, html, "utf-8");
18
- console.log(`\n📊 Image diff report: ${outputPath}`);
19
- if (autoOpen) exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} "${outputPath}"`);
20
- }
21
- /** Copy images to report dir, preserving directory structure relative to configRoot */
22
- async function copyImagesToReport(failures, reportDir, configRoot) {
23
- return Promise.all(failures.map(async (failure) => {
24
- const copiedPaths = await copyImageSet(failure.paths, reportDir, configRoot);
25
- return {
26
- ...failure,
27
- paths: copiedPaths
28
- };
29
- }));
30
- }
31
- async function copyImageSet(paths, reportDir, configRoot) {
32
- const copy = async (sourcePath) => {
33
- const relativePath = path.relative(configRoot, sourcePath);
34
- const destPath = path.join(reportDir, relativePath);
35
- const destDir = path.dirname(destPath);
36
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
37
- await fs.promises.copyFile(sourcePath, destPath);
38
- return relativePath;
39
- };
40
- return {
41
- reference: await copy(paths.reference),
42
- actual: await copy(paths.actual),
43
- diff: await copy(paths.diff)
44
- };
45
- }
46
- function createReportHTML(failures) {
47
- const timestamp = (/* @__PURE__ */ new Date()).toLocaleString();
48
- const totalFailures = failures.length;
49
- const rows = failures.map((failure) => {
50
- const { testName, snapshotName, comparison, paths } = failure;
51
- const { mismatchedPixels, mismatchedPixelRatio } = comparison;
52
- return `
53
- <tr>
54
- <td class="test-name">
55
- <strong>${escapeHtml(testName)}</strong><br>
56
- <code>${escapeHtml(snapshotName)}</code>
57
- </td>
58
- <td class="image-cell">
59
- <a href="${paths.reference}" target="_blank">
60
- <img src="${paths.reference}" alt="Expected" />
61
- </a>
62
- <div class="label">Expected</div>
63
- </td>
64
- <td class="image-cell">
65
- <a href="${paths.actual}" target="_blank">
66
- <img src="${paths.actual}" alt="Actual" />
67
- </a>
68
- <div class="label">Actual</div>
69
- </td>
70
- <td class="image-cell">
71
- <a href="${paths.diff}" target="_blank">
72
- <img src="${paths.diff}" alt="Diff" />
73
- </a>
74
- <div class="label">Diff</div>
75
- </td>
76
- <td class="stats">
77
- <div><strong>${mismatchedPixels}</strong> pixels</div>
78
- <div><strong>${(mismatchedPixelRatio * 100).toFixed(2)}%</strong></div>
79
- </td>
80
- </tr>
81
- `;
82
- }).join("\n");
83
- return `<!DOCTYPE html>
84
- <html lang="en">
85
- <head>
86
- <meta charset="UTF-8">
87
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
- <title>Image Snapshot Failures - ${totalFailures} failed</title>
89
- <style>
90
- * { margin: 0; padding: 0; box-sizing: border-box; }
91
- body {
92
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
93
- padding: 20px;
94
- background: #f5f5f5;
95
- }
96
- header {
97
- background: white;
98
- padding: 20px;
99
- border-radius: 8px;
100
- margin-bottom: 20px;
101
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
102
- }
103
- h1 {
104
- color: #d73027;
105
- margin-bottom: 10px;
106
- }
107
- .meta {
108
- color: #666;
109
- font-size: 14px;
110
- }
111
- .update-hint {
112
- background: #fff3cd;
113
- border: 1px solid #ffc107;
114
- border-radius: 4px;
115
- padding: 12px;
116
- margin: 15px 0;
117
- }
118
- .update-hint code {
119
- background: #f8f9fa;
120
- padding: 2px 6px;
121
- border-radius: 3px;
122
- font-family: 'Monaco', 'Courier New', monospace;
123
- }
124
- table {
125
- width: 100%;
126
- border-collapse: collapse;
127
- background: white;
128
- border-radius: 8px;
129
- overflow: hidden;
130
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
131
- }
132
- th, td {
133
- padding: 15px;
134
- text-align: left;
135
- border-bottom: 1px solid #e0e0e0;
136
- }
137
- th {
138
- background: #f8f9fa;
139
- font-weight: 600;
140
- color: #333;
141
- text-align: center;
142
- }
143
- tr:last-child td {
144
- border-bottom: none;
145
- }
146
- tr:hover {
147
- background: #fafafa;
148
- }
149
- .test-name {
150
- min-width: 200px;
151
- vertical-align: top;
152
- }
153
- .test-name strong {
154
- color: #333;
155
- }
156
- .test-name code {
157
- color: #666;
158
- font-size: 12px;
159
- background: #f5f5f5;
160
- padding: 2px 6px;
161
- border-radius: 3px;
162
- }
163
- .image-cell {
164
- text-align: center;
165
- vertical-align: top;
166
- width: 25%;
167
- }
168
- .image-cell a {
169
- display: block;
170
- text-decoration: none;
171
- }
172
- .image-cell img {
173
- max-width: 100%;
174
- height: auto;
175
- max-height: 300px;
176
- border: 1px solid #ddd;
177
- border-radius: 4px;
178
- cursor: pointer;
179
- transition: transform 0.2s;
180
- }
181
- .image-cell img:hover {
182
- transform: scale(1.02);
183
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
184
- }
185
- .label {
186
- margin-top: 8px;
187
- font-size: 12px;
188
- color: #666;
189
- font-weight: 500;
190
- }
191
- .stats {
192
- text-align: center;
193
- color: #d73027;
194
- font-size: 14px;
195
- vertical-align: top;
196
- }
197
- .stats div {
198
- margin: 5px 0;
199
- }
200
- footer {
201
- margin-top: 20px;
202
- text-align: center;
203
- color: #999;
204
- font-size: 12px;
205
- }
206
- </style>
207
- </head>
208
- <body>
209
- <header>
210
- <h1>🔴 Image Snapshot Failures</h1>
211
- <div class="meta">
212
- <strong>${totalFailures}</strong> test${totalFailures === 1 ? "" : "s"} failed •
213
- Generated: ${escapeHtml(timestamp)}
214
- </div>
215
- <div class="update-hint">
216
- 💡 <strong>To update snapshots:</strong> Run <code>pnpm test -- -u</code> or <code>vitest -u</code>
217
- </div>
218
- </header>
219
-
220
- <table>
221
- <thead>
222
- <tr>
223
- <th>Test</th>
224
- <th>Expected</th>
225
- <th>Actual</th>
226
- <th>Diff</th>
227
- <th>Mismatch</th>
228
- </tr>
229
- </thead>
230
- <tbody>
231
- ${rows}
232
- </tbody>
233
- </table>
234
-
235
- <footer>
236
- Click images to view full size •
237
- Diff images highlight mismatched pixels in yellow/red
238
- </footer>
239
- </body>
240
- </html>`;
241
- }
242
- function escapeHtml(text) {
243
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
244
- }
245
-
246
- //#endregion
247
9
  //#region src/ImageComparison.ts
248
10
  async function compareImages(reference, actual, options = {}) {
249
11
  const { threshold = .1, allowedPixelRatio = 0, allowedPixels = 0, includeAA = true } = options;
@@ -436,63 +198,5 @@ async function missingSnapshot(manager, actualBuffer, snapshotName) {
436
198
  };
437
199
  }
438
200
 
439
- //#endregion
440
- //#region src/ImageSnapshotReporter.ts
441
- /** Vitest reporter that generates HTML diff reports for image snapshot failures */
442
- var ImageSnapshotReporter = class {
443
- failures = [];
444
- vitest;
445
- reportPath;
446
- autoOpen;
447
- constructor(options = {}) {
448
- this.reportPath = options.reportPath;
449
- this.autoOpen = options.autoOpen ?? process.env.IMAGE_DIFF_AUTO_OPEN === "true";
450
- }
451
- onInit(vitest) {
452
- this.vitest = vitest;
453
- }
454
- onTestCaseResult(testCase) {
455
- const result = testCase.result();
456
- if (result?.state !== "failed") return;
457
- const meta = testCase.meta();
458
- if (!meta.imageSnapshotFailure) return;
459
- const error = result.errors?.[0];
460
- const failure = captureFailure(testCase, meta.imageSnapshotFailure, error?.message || "");
461
- this.failures.push(failure);
462
- }
463
- async onTestRunEnd() {
464
- if (this.failures.length === 0) return;
465
- const reportDir = this.resolveReportDir();
466
- await generateDiffReport(this.failures, {
467
- autoOpen: this.autoOpen,
468
- reportDir,
469
- configRoot: this.vitest.config.root
470
- });
471
- }
472
- resolveReportDir() {
473
- const configRoot = this.vitest.config.root;
474
- if (!this.reportPath) return path.join(configRoot, "__image_diff_report__");
475
- return path.isAbsolute(this.reportPath) ? this.reportPath : path.join(configRoot, this.reportPath);
476
- }
477
- };
478
- function captureFailure(testCase, data, message) {
479
- const snapshotName = data.actualPath.match(/([^/]+)\.png$/)?.[1] || "unknown";
480
- return {
481
- testName: testCase.fullName || testCase.name,
482
- snapshotName,
483
- comparison: {
484
- pass: false,
485
- message,
486
- mismatchedPixels: data.mismatchedPixels,
487
- mismatchedPixelRatio: data.mismatchedPixelRatio
488
- },
489
- paths: {
490
- reference: data.expectedPath,
491
- actual: data.actualPath,
492
- diff: data.diffPath
493
- }
494
- };
495
- }
496
-
497
201
  //#endregion
498
202
  export { ImageSnapshotManager, ImageSnapshotReporter, compareImages, generateDiffReport, imageMatcher, pngBuffer };
@@ -0,0 +1,2 @@
1
+ import { t as ImageSnapshotReporter } from "./ImageSnapshotReporter-C3C7yfn1.js";
2
+ export { ImageSnapshotReporter as default };
@@ -0,0 +1,7 @@
1
+ import { t as ImageSnapshotReporter } from "./ImageSnapshotReporter-D1-9tyxx.js";
2
+
3
+ //#region src/reporter.ts
4
+ var reporter_default = ImageSnapshotReporter;
5
+
6
+ //#endregion
7
+ export { reporter_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-image-snapshot",
3
- "version": "0.6.22",
3
+ "version": "0.6.23",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
package/src/DiffReport.ts CHANGED
@@ -34,9 +34,7 @@ export async function generateDiffReport(
34
34
 
35
35
  if (failures.length === 0) return;
36
36
 
37
- if (!fs.existsSync(reportDir)) {
38
- fs.mkdirSync(reportDir, { recursive: true });
39
- }
37
+ await fs.promises.mkdir(reportDir, { recursive: true });
40
38
 
41
39
  const withCopiedImages = await copyImagesToReport(
42
40
  failures,
@@ -84,24 +82,28 @@ async function copyImageSet(
84
82
  reportDir: string,
85
83
  configRoot: string,
86
84
  ): Promise<{ reference: string; actual: string; diff: string }> {
87
- const copy = async (sourcePath: string): Promise<string> => {
88
- const relativePath = path.relative(configRoot, sourcePath);
89
- const destPath = path.join(reportDir, relativePath);
90
- const destDir = path.dirname(destPath);
85
+ return {
86
+ reference: await copyImage(paths.reference, reportDir, configRoot),
87
+ actual: await copyImage(paths.actual, reportDir, configRoot),
88
+ diff: await copyImage(paths.diff, reportDir, configRoot),
89
+ };
90
+ }
91
91
 
92
- if (!fs.existsSync(destDir)) {
93
- fs.mkdirSync(destDir, { recursive: true });
94
- }
92
+ /** Copy single image to report dir, preserving directory structure relative to configRoot */
93
+ async function copyImage(
94
+ sourcePath: string,
95
+ reportDir: string,
96
+ configRoot: string,
97
+ ): Promise<string> {
98
+ if (!fs.existsSync(sourcePath)) return "";
95
99
 
96
- await fs.promises.copyFile(sourcePath, destPath);
97
- return relativePath;
98
- };
100
+ const relativePath = path.relative(configRoot, sourcePath);
101
+ const destPath = path.join(reportDir, relativePath);
102
+ const destDir = path.dirname(destPath);
99
103
 
100
- return {
101
- reference: await copy(paths.reference),
102
- actual: await copy(paths.actual),
103
- diff: await copy(paths.diff),
104
- };
104
+ await fs.promises.mkdir(destDir, { recursive: true });
105
+ await fs.promises.copyFile(sourcePath, destPath);
106
+ return relativePath;
105
107
  }
106
108
 
107
109
  function createReportHTML(failures: ImageSnapshotFailure[]): string {
@@ -132,10 +134,7 @@ function createReportHTML(failures: ImageSnapshotFailure[]): string {
132
134
  <div class="label">Actual</div>
133
135
  </td>
134
136
  <td class="image-cell">
135
- <a href="${paths.diff}" target="_blank">
136
- <img src="${paths.diff}" alt="Diff" />
137
- </a>
138
- <div class="label">Diff</div>
137
+ ${diffCellHTML(paths.diff)}
139
138
  </td>
140
139
  <td class="stats">
141
140
  <div><strong>${mismatchedPixels}</strong> pixels</div>
@@ -254,6 +253,13 @@ function createReportHTML(failures: ImageSnapshotFailure[]): string {
254
253
  color: #666;
255
254
  font-weight: 500;
256
255
  }
256
+ .no-diff {
257
+ color: #999;
258
+ font-size: 13px;
259
+ font-style: italic;
260
+ padding: 20px;
261
+ text-align: center;
262
+ }
257
263
  .stats {
258
264
  text-align: center;
259
265
  color: #d73027;
@@ -314,3 +320,13 @@ function escapeHtml(text: string): string {
314
320
  .replace(/"/g, "&quot;")
315
321
  .replace(/'/g, "&#039;");
316
322
  }
323
+
324
+ function diffCellHTML(diffPath: string): string {
325
+ if (!diffPath) {
326
+ return `<div class="no-diff">No diff image<br/>(dimension mismatch)</div>`;
327
+ }
328
+ return `<a href="${diffPath}" target="_blank">
329
+ <img src="${diffPath}" alt="Diff" />
330
+ </a>
331
+ <div class="label">Diff</div>`;
332
+ }
@@ -106,6 +106,13 @@
106
106
  color: #666;
107
107
  font-weight: 500;
108
108
  }
109
+ .no-diff {
110
+ color: #999;
111
+ font-size: 13px;
112
+ font-style: italic;
113
+ padding: 20px;
114
+ text-align: center;
115
+ }
109
116
  .stats {
110
117
  text-align: center;
111
118
  color: #d73027;
@@ -128,7 +135,7 @@
128
135
  <h1>🔴 Image Snapshot Failures</h1>
129
136
  <div class="meta">
130
137
  <strong>1</strong> test failed •
131
- Generated: 10/30/2025, 8:09:42 AM
138
+ Generated: 11/4/2025, 4:23:56 PM
132
139
  </div>
133
140
  <div class="update-hint">
134
141
  💡 <strong>To update snapshots:</strong> Run <code>pnpm test -- -u</code> or <code>vitest -u</code>
@@ -153,20 +160,20 @@
153
160
  <code>red-square</code>
154
161
  </td>
155
162
  <td class="image-cell">
156
- <a href="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png" target="_blank">
157
- <img src="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png" alt="Expected" />
163
+ <a href="src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png" target="_blank">
164
+ <img src="src/test/fixtures/failing-snapshot/__image_snapshots__/red-square.png" alt="Expected" />
158
165
  </a>
159
166
  <div class="label">Expected</div>
160
167
  </td>
161
168
  <td class="image-cell">
162
- <a href="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_actual__/red-square.png" target="_blank">
163
- <img src="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_actual__/red-square.png" alt="Actual" />
169
+ <a href="src/test/fixtures/failing-snapshot/__image_actual__/red-square.png" target="_blank">
170
+ <img src="src/test/fixtures/failing-snapshot/__image_actual__/red-square.png" alt="Actual" />
164
171
  </a>
165
172
  <div class="label">Actual</div>
166
173
  </td>
167
174
  <td class="image-cell">
168
- <a href="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png" target="_blank">
169
- <img src="packages/vitest-image-snapshot/src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png" alt="Diff" />
175
+ <a href="src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png" target="_blank">
176
+ <img src="src/test/fixtures/failing-snapshot/__image_diffs__/red-square.png" alt="Diff" />
170
177
  </a>
171
178
  <div class="label">Diff</div>
172
179
  </td>