vitest-image-snapshot 0.6.49 → 0.6.51
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 +1 -1
- package/dist/{ImageSnapshotReporter-D7WPlzM3.js → ImageSnapshotReporter-B3evIi81.js} +25 -18
- package/dist/ImageSnapshotReporter-D6HJydo6.d.ts +91 -0
- package/dist/index.d.ts +2 -58
- package/dist/index.js +11 -5
- package/dist/reporter.d.ts +1 -1
- package/dist/reporter.js +1 -1
- package/package.json +6 -2
- package/src/DiffReport.ts +11 -16
- package/src/ImageSnapshotReporter.ts +25 -28
- package/src/SnapshotManager.ts +18 -3
- package/src/core/index.ts +4 -0
- package/src/test/SnapshotManager.test.ts +24 -0
- package/dist/ImageSnapshotReporter-D3jHkOX3.d.ts +0 -35
package/README.md
CHANGED
|
@@ -246,7 +246,7 @@ test("canvas output matches snapshot", async () => {
|
|
|
246
246
|
|
|
247
247
|
## Build Version
|
|
248
248
|
**vitest-image-snapshot** is currently
|
|
249
|
-
part of the [wesl-js](https://github.com/
|
|
249
|
+
part of the [wesl-js](https://github.com/webgpu-tools/wesl-js/) monorepo.
|
|
250
250
|
(It'll eventually move to it's own repo).
|
|
251
251
|
|
|
252
252
|
## Contributions
|
|
@@ -5,7 +5,13 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import * as http from "node:http";
|
|
6
6
|
import isCI from "is-ci";
|
|
7
7
|
//#region src/DiffReport.ts
|
|
8
|
-
const
|
|
8
|
+
const autoOpenValues = [
|
|
9
|
+
"always",
|
|
10
|
+
"failures",
|
|
11
|
+
"never"
|
|
12
|
+
];
|
|
13
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const templatesDir = path.join(thisDir, "templates");
|
|
9
15
|
/** Clear the diff report directory if it exists. */
|
|
10
16
|
async function clearDiffReport(reportDir) {
|
|
11
17
|
if (fs.existsSync(reportDir)) await fs.promises.rm(reportDir, { recursive: true });
|
|
@@ -25,7 +31,7 @@ async function generateDiffReport(failures, config) {
|
|
|
25
31
|
const outputPath = path.join(reportDir, "index.html");
|
|
26
32
|
await fs.promises.writeFile(outputPath, html, "utf-8");
|
|
27
33
|
if (failures.length > 0 && !liveReload) console.log(`\n Image diff report: ${outputPath}`);
|
|
28
|
-
if (autoOpen === "
|
|
34
|
+
if (!(autoOpen === "always" || autoOpen === "failures" && failures.length > 0)) return;
|
|
29
35
|
try {
|
|
30
36
|
exec(`${{
|
|
31
37
|
darwin: "open",
|
|
@@ -82,7 +88,7 @@ function createReportHTML(failures, liveReload) {
|
|
|
82
88
|
timestamp: escapeHtml(timestamp),
|
|
83
89
|
script
|
|
84
90
|
});
|
|
85
|
-
const rows = failures.map(
|
|
91
|
+
const rows = failures.map(createRowHTML).join("\n");
|
|
86
92
|
return renderTemplate(loadTemplate("report-failure.hbs"), {
|
|
87
93
|
totalFailures: String(totalFailures),
|
|
88
94
|
testPlural: totalFailures === 1 ? "test" : "tests",
|
|
@@ -133,11 +139,6 @@ function diffCellHTML(diffPath) {
|
|
|
133
139
|
}
|
|
134
140
|
//#endregion
|
|
135
141
|
//#region src/ImageSnapshotReporter.ts
|
|
136
|
-
const autoOpenValues = [
|
|
137
|
-
"always",
|
|
138
|
-
"failures",
|
|
139
|
-
"never"
|
|
140
|
-
];
|
|
141
142
|
/** Vitest reporter that generates HTML diff reports for image snapshot failures */
|
|
142
143
|
var ImageSnapshotReporter = class {
|
|
143
144
|
failuresByFile = /* @__PURE__ */ new Map();
|
|
@@ -171,7 +172,7 @@ var ImageSnapshotReporter = class {
|
|
|
171
172
|
startServer() {
|
|
172
173
|
const reportDir = this.resolveReportDir();
|
|
173
174
|
const server = http.createServer((req, res) => {
|
|
174
|
-
const url = req.url === "/" ? "/index.html" : req.url
|
|
175
|
+
const url = !req.url || req.url === "/" ? "/index.html" : req.url;
|
|
175
176
|
const filePath = path.join(reportDir, url);
|
|
176
177
|
fs.stat(filePath, (statErr, stats) => {
|
|
177
178
|
if (statErr) {
|
|
@@ -180,7 +181,12 @@ var ImageSnapshotReporter = class {
|
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
183
|
const ext = path.extname(filePath);
|
|
183
|
-
const contentType =
|
|
184
|
+
const contentType = {
|
|
185
|
+
".html": "text/html",
|
|
186
|
+
".css": "text/css",
|
|
187
|
+
".png": "image/png",
|
|
188
|
+
".js": "text/javascript"
|
|
189
|
+
}[ext] ?? "application/octet-stream";
|
|
184
190
|
const headers = {
|
|
185
191
|
"Content-Type": contentType,
|
|
186
192
|
"Last-Modified": stats.mtime.toUTCString()
|
|
@@ -213,11 +219,10 @@ var ImageSnapshotReporter = class {
|
|
|
213
219
|
onTestCaseResult(testCase) {
|
|
214
220
|
const result = testCase.result();
|
|
215
221
|
if (result?.state !== "failed") return;
|
|
216
|
-
const
|
|
217
|
-
if (!
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
const moduleId = testCase.module.moduleId;
|
|
222
|
+
const { imageSnapshotFailure } = testCase.meta();
|
|
223
|
+
if (!imageSnapshotFailure) return;
|
|
224
|
+
const failure = captureFailure(testCase, imageSnapshotFailure, result.errors?.[0]?.message || "");
|
|
225
|
+
const { moduleId } = testCase.module;
|
|
221
226
|
const existing = this.failuresByFile.get(moduleId) || [];
|
|
222
227
|
this.failuresByFile.set(moduleId, [...existing, failure]);
|
|
223
228
|
}
|
|
@@ -235,16 +240,18 @@ var ImageSnapshotReporter = class {
|
|
|
235
240
|
return path.isAbsolute(this.reportPath) ? this.reportPath : path.join(configRoot, this.reportPath);
|
|
236
241
|
}
|
|
237
242
|
};
|
|
243
|
+
/** Build a failure record from test metadata for the diff report. */
|
|
238
244
|
function captureFailure(testCase, data, message) {
|
|
239
245
|
const snapshotName = data.actualPath.match(/([^/]+)\.png$/)?.[1] || "unknown";
|
|
246
|
+
const { mismatchedPixels, mismatchedPixelRatio } = data;
|
|
240
247
|
return {
|
|
241
248
|
testName: testCase.fullName || testCase.name,
|
|
242
249
|
snapshotName,
|
|
243
250
|
comparison: {
|
|
244
251
|
pass: false,
|
|
245
252
|
message,
|
|
246
|
-
mismatchedPixels
|
|
247
|
-
mismatchedPixelRatio
|
|
253
|
+
mismatchedPixels,
|
|
254
|
+
mismatchedPixelRatio
|
|
248
255
|
},
|
|
249
256
|
paths: {
|
|
250
257
|
reference: data.expectedPath,
|
|
@@ -254,4 +261,4 @@ function captureFailure(testCase, data, message) {
|
|
|
254
261
|
};
|
|
255
262
|
}
|
|
256
263
|
//#endregion
|
|
257
|
-
export {
|
|
264
|
+
export { generateDiffReport as i, autoOpenValues as n, clearDiffReport as r, ImageSnapshotReporter as t };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Reporter, TestCase, TestSpecification, 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
|
+
declare const autoOpenValues: readonly ["always", "failures", "never"];
|
|
33
|
+
type AutoOpen = (typeof autoOpenValues)[number];
|
|
34
|
+
/** Failed image snapshot with comparison results and file paths. */
|
|
35
|
+
interface ImageSnapshotFailure {
|
|
36
|
+
testName: string;
|
|
37
|
+
snapshotName: string;
|
|
38
|
+
comparison: ComparisonResult;
|
|
39
|
+
paths: {
|
|
40
|
+
reference: string;
|
|
41
|
+
actual: string;
|
|
42
|
+
diff: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Configuration for HTML diff report generation. */
|
|
46
|
+
interface DiffReportConfig {
|
|
47
|
+
/** Auto-open report in browser. Default: "failures" or "never" in CI */
|
|
48
|
+
autoOpen: AutoOpen;
|
|
49
|
+
/** Directory path for generated HTML report (absolute or relative). */
|
|
50
|
+
reportDir: string;
|
|
51
|
+
/** Vitest config root for calculating relative paths when copying images */
|
|
52
|
+
configRoot: string;
|
|
53
|
+
/** Enable live reload script in HTML. Default: false */
|
|
54
|
+
liveReload?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Clear the diff report directory if it exists. */
|
|
57
|
+
declare function clearDiffReport(reportDir: string): Promise<void>;
|
|
58
|
+
/** Generate HTML diff report for image snapshot results. */
|
|
59
|
+
declare function generateDiffReport(failures: ImageSnapshotFailure[], config: DiffReportConfig): Promise<void>;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/ImageSnapshotReporter.d.ts
|
|
62
|
+
interface ImageSnapshotReporterOptions {
|
|
63
|
+
/** Auto-open report in browser. Default: "failures" or "never" in CI */
|
|
64
|
+
autoOpen?: AutoOpen;
|
|
65
|
+
/** Report directory (relative to config.root or absolute) */
|
|
66
|
+
reportPath?: string;
|
|
67
|
+
/** Port for live-reload server. Set to 0 to disable. Default: 4343 */
|
|
68
|
+
port?: number;
|
|
69
|
+
}
|
|
70
|
+
/** Vitest reporter that generates HTML diff reports for image snapshot failures */
|
|
71
|
+
declare class ImageSnapshotReporter implements Reporter {
|
|
72
|
+
private failuresByFile;
|
|
73
|
+
private vitest;
|
|
74
|
+
private reportPath?;
|
|
75
|
+
private autoOpen;
|
|
76
|
+
private port;
|
|
77
|
+
private serverStarted;
|
|
78
|
+
constructor(options?: ImageSnapshotReporterOptions);
|
|
79
|
+
/** Resolve autoOpen setting with priority: CI override > env var > config option > default */
|
|
80
|
+
private resolveAutoOpen;
|
|
81
|
+
/** Parse and validate IMAGE_DIFF_AUTO_OPEN environment variable */
|
|
82
|
+
private envAutoOpen;
|
|
83
|
+
onInit(vitest: Vitest): void;
|
|
84
|
+
private startServer;
|
|
85
|
+
onTestRunStart(specifications: ReadonlyArray<TestSpecification>): void;
|
|
86
|
+
onTestCaseResult(testCase: TestCase): void;
|
|
87
|
+
onTestRunEnd(): Promise<void>;
|
|
88
|
+
private resolveReportDir;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
export { ImageSnapshotFailure as a, generateDiffReport as c, ImageData as d, compareImages as f, DiffReportConfig as i, ComparisonOptions as l, ImageSnapshotReporterOptions as n, autoOpenValues as o, AutoOpen as r, clearDiffReport as s, ImageSnapshotReporter as t, ComparisonResult as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,61 +1,5 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { a as ImageSnapshotFailure, c as generateDiffReport, d as ImageData, f as compareImages, i as DiffReportConfig, l as ComparisonOptions, n as ImageSnapshotReporterOptions, o as autoOpenValues, r as AutoOpen, s as clearDiffReport, t as ImageSnapshotReporter, u as ComparisonResult } from "./ImageSnapshotReporter-D6HJydo6.js";
|
|
2
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: "failures" or "never" in CI */
|
|
46
|
-
autoOpen: AutoOpen;
|
|
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
|
-
/** Enable live reload script in HTML. Default: false */
|
|
52
|
-
liveReload?: boolean;
|
|
53
|
-
}
|
|
54
|
-
/** Clear the diff report directory if it exists. */
|
|
55
|
-
declare function clearDiffReport(reportDir: string): Promise<void>;
|
|
56
|
-
/** Generate HTML diff report for image snapshot results. */
|
|
57
|
-
declare function generateDiffReport(failures: ImageSnapshotFailure[], config: DiffReportConfig): Promise<void>;
|
|
58
|
-
//#endregion
|
|
59
3
|
//#region src/ImageSnapshotMatcher.d.ts
|
|
60
4
|
interface MatchImageOptions extends ComparisonOptions {
|
|
61
5
|
name?: string;
|
|
@@ -101,4 +45,4 @@ declare module "vitest" {
|
|
|
101
45
|
}
|
|
102
46
|
}
|
|
103
47
|
//#endregion
|
|
104
|
-
export { AutoOpen, ComparisonOptions, ComparisonResult, DiffReportConfig, ImageData, ImageSnapshotFailure, ImageSnapshotManager, ImageSnapshotReporter, ImageSnapshotReporterOptions, MatchImageOptions, SnapshotConfig, clearDiffReport, compareImages, generateDiffReport, imageMatcher, pngBuffer };
|
|
48
|
+
export { AutoOpen, ComparisonOptions, ComparisonResult, DiffReportConfig, ImageData, ImageSnapshotFailure, ImageSnapshotManager, ImageSnapshotReporter, ImageSnapshotReporterOptions, MatchImageOptions, SnapshotConfig, autoOpenValues, clearDiffReport, compareImages, generateDiffReport, imageMatcher, pngBuffer };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { i as generateDiffReport, n as autoOpenValues, r as clearDiffReport, t as ImageSnapshotReporter } from "./ImageSnapshotReporter-B3evIi81.js";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import pixelmatch from "pixelmatch";
|
|
@@ -72,6 +72,8 @@ function pngBuffer(imageData) {
|
|
|
72
72
|
}
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/SnapshotManager.ts
|
|
75
|
+
const replaceableChars = /[<>:"/\\|?*]/g;
|
|
76
|
+
const unreplaceableChars = /\p{Control}+/gu;
|
|
75
77
|
/** Manage reference/actual/diff snapshot files. */
|
|
76
78
|
var ImageSnapshotManager = class {
|
|
77
79
|
config;
|
|
@@ -87,13 +89,13 @@ var ImageSnapshotManager = class {
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
referencePath(snapshotName) {
|
|
90
|
-
return path.join(this.testDir, this.config.snapshotDir, `${snapshotName}.png`);
|
|
92
|
+
return path.join(this.testDir, this.config.snapshotDir, `${sanitizeName(snapshotName)}.png`);
|
|
91
93
|
}
|
|
92
94
|
actualPath(snapshotName) {
|
|
93
|
-
return path.join(this.testDir, this.config.actualDir, `${snapshotName}.png`);
|
|
95
|
+
return path.join(this.testDir, this.config.actualDir, `${sanitizeName(snapshotName)}.png`);
|
|
94
96
|
}
|
|
95
97
|
diffPath(snapshotName) {
|
|
96
|
-
return path.join(this.testDir, this.config.diffDir, `${snapshotName}.png`);
|
|
98
|
+
return path.join(this.testDir, this.config.diffDir, `${sanitizeName(snapshotName)}.png`);
|
|
97
99
|
}
|
|
98
100
|
/** Update failing snapshots (only in "all" mode with vitest -u) */
|
|
99
101
|
shouldUpdate() {
|
|
@@ -123,6 +125,10 @@ var ImageSnapshotManager = class {
|
|
|
123
125
|
await fs.promises.writeFile(filepath, buffer);
|
|
124
126
|
}
|
|
125
127
|
};
|
|
128
|
+
/** Replace chars that are invalid in Windows filenames. */
|
|
129
|
+
function sanitizeName(input) {
|
|
130
|
+
return input.replace(replaceableChars, "_").replace(unreplaceableChars, "").trim();
|
|
131
|
+
}
|
|
126
132
|
//#endregion
|
|
127
133
|
//#region src/ImageSnapshotMatcher.ts
|
|
128
134
|
/** Register toMatchImage() matcher with Vitest */
|
|
@@ -193,4 +199,4 @@ async function missingSnapshot(manager, actualBuffer, snapshotName) {
|
|
|
193
199
|
};
|
|
194
200
|
}
|
|
195
201
|
//#endregion
|
|
196
|
-
export { ImageSnapshotManager, ImageSnapshotReporter, clearDiffReport, compareImages, generateDiffReport, imageMatcher, pngBuffer };
|
|
202
|
+
export { ImageSnapshotManager, ImageSnapshotReporter, autoOpenValues, clearDiffReport, compareImages, generateDiffReport, imageMatcher, pngBuffer };
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as ImageSnapshotReporter } from "./ImageSnapshotReporter-D6HJydo6.js";
|
|
2
2
|
export { ImageSnapshotReporter as default };
|
package/dist/reporter.js
CHANGED
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-image-snapshot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"src"
|
|
8
8
|
],
|
|
9
|
-
"repository": "github:
|
|
9
|
+
"repository": "github:webgpu-tools/wesl-js",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
+
"./core": {
|
|
16
|
+
"types": "./dist/core/index.d.ts",
|
|
17
|
+
"import": "./dist/core/index.js"
|
|
18
|
+
},
|
|
15
19
|
"./reporter": {
|
|
16
20
|
"types": "./dist/reporter.d.ts",
|
|
17
21
|
"import": "./dist/reporter.js"
|
package/src/DiffReport.ts
CHANGED
|
@@ -3,12 +3,12 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { ComparisonResult } from "./ImageComparison.ts";
|
|
6
|
-
import type { AutoOpen } from "./ImageSnapshotReporter.ts";
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
7
|
+
export const autoOpenValues = ["always", "failures", "never"] as const;
|
|
8
|
+
export type AutoOpen = (typeof autoOpenValues)[number];
|
|
9
|
+
|
|
10
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const templatesDir = path.join(thisDir, "templates");
|
|
12
12
|
|
|
13
13
|
/** Failed image snapshot with comparison results and file paths. */
|
|
14
14
|
export interface ImageSnapshotFailure {
|
|
@@ -64,11 +64,11 @@ export async function generateDiffReport(
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const copied =
|
|
68
68
|
failures.length > 0
|
|
69
69
|
? await copyImagesToReport(failures, reportDir, configRoot)
|
|
70
70
|
: [];
|
|
71
|
-
const html = createReportHTML(
|
|
71
|
+
const html = createReportHTML(copied, liveReload);
|
|
72
72
|
const outputPath = path.join(reportDir, "index.html");
|
|
73
73
|
|
|
74
74
|
await fs.promises.writeFile(outputPath, html, "utf-8");
|
|
@@ -78,14 +78,10 @@ export async function generateDiffReport(
|
|
|
78
78
|
console.log(`\n Image diff report: ${outputPath}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
autoOpen === "
|
|
83
|
-
|
|
84
|
-
) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
81
|
+
const shouldOpen =
|
|
82
|
+
autoOpen === "always" || (autoOpen === "failures" && failures.length > 0);
|
|
83
|
+
if (!shouldOpen) return;
|
|
87
84
|
|
|
88
|
-
// open the browser to view the report
|
|
89
85
|
try {
|
|
90
86
|
const commands: Record<string, string> = { darwin: "open", win32: "start" };
|
|
91
87
|
const cmd = commands[process.platform] ?? "xdg-open";
|
|
@@ -145,7 +141,6 @@ function renderTemplate(
|
|
|
145
141
|
result = result.replaceAll(`{{${key}}}`, value);
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
// Check for any unreplaced placeholders
|
|
149
144
|
const unreplaced = result.match(/\{\{(\w+)\}\}/g);
|
|
150
145
|
if (unreplaced) {
|
|
151
146
|
const keys = unreplaced.map(m => m.slice(2, -2)).join(", ");
|
|
@@ -170,7 +165,7 @@ function createReportHTML(
|
|
|
170
165
|
});
|
|
171
166
|
}
|
|
172
167
|
|
|
173
|
-
const rows = failures.map(
|
|
168
|
+
const rows = failures.map(createRowHTML).join("\n");
|
|
174
169
|
|
|
175
170
|
return renderTemplate(loadTemplate("report-failure.hbs"), {
|
|
176
171
|
totalFailures: String(totalFailures),
|
|
@@ -8,10 +8,12 @@ import type {
|
|
|
8
8
|
TestSpecification,
|
|
9
9
|
Vitest,
|
|
10
10
|
} from "vitest/node";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
type AutoOpen,
|
|
13
|
+
autoOpenValues,
|
|
14
|
+
generateDiffReport,
|
|
15
|
+
type ImageSnapshotFailure,
|
|
16
|
+
} from "./DiffReport.ts";
|
|
15
17
|
|
|
16
18
|
/** Metadata captured when image snapshot test fails, used to generate HTML report. */
|
|
17
19
|
interface ImageSnapshotFailureData {
|
|
@@ -78,7 +80,7 @@ export class ImageSnapshotReporter implements Reporter {
|
|
|
78
80
|
private startServer() {
|
|
79
81
|
const reportDir = this.resolveReportDir();
|
|
80
82
|
const server = http.createServer((req, res) => {
|
|
81
|
-
const url = req.url === "/" ? "/index.html" : req.url
|
|
83
|
+
const url = !req.url || req.url === "/" ? "/index.html" : req.url;
|
|
82
84
|
const filePath = path.join(reportDir, url);
|
|
83
85
|
|
|
84
86
|
fs.stat(filePath, (statErr, stats) => {
|
|
@@ -89,14 +91,13 @@ export class ImageSnapshotReporter implements Reporter {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
const ext = path.extname(filePath);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: "application/octet-stream";
|
|
94
|
+
const mimeTypes: Record<string, string> = {
|
|
95
|
+
".html": "text/html",
|
|
96
|
+
".css": "text/css",
|
|
97
|
+
".png": "image/png",
|
|
98
|
+
".js": "text/javascript",
|
|
99
|
+
};
|
|
100
|
+
const contentType = mimeTypes[ext] ?? "application/octet-stream";
|
|
100
101
|
const headers = {
|
|
101
102
|
"Content-Type": contentType,
|
|
102
103
|
"Last-Modified": stats.mtime.toUTCString(),
|
|
@@ -140,18 +141,13 @@ export class ImageSnapshotReporter implements Reporter {
|
|
|
140
141
|
const result = testCase.result();
|
|
141
142
|
if (result?.state !== "failed") return;
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
testCase,
|
|
151
|
-
meta.imageSnapshotFailure,
|
|
152
|
-
error?.message || "",
|
|
153
|
-
);
|
|
154
|
-
const moduleId = testCase.module.moduleId;
|
|
144
|
+
type Meta = { imageSnapshotFailure?: ImageSnapshotFailureData };
|
|
145
|
+
const { imageSnapshotFailure } = testCase.meta() as Meta;
|
|
146
|
+
if (!imageSnapshotFailure) return;
|
|
147
|
+
|
|
148
|
+
const message = result.errors?.[0]?.message || "";
|
|
149
|
+
const failure = captureFailure(testCase, imageSnapshotFailure, message);
|
|
150
|
+
const { moduleId } = testCase.module;
|
|
155
151
|
const existing = this.failuresByFile.get(moduleId) || [];
|
|
156
152
|
this.failuresByFile.set(moduleId, [...existing, failure]);
|
|
157
153
|
}
|
|
@@ -177,21 +173,22 @@ export class ImageSnapshotReporter implements Reporter {
|
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
175
|
|
|
176
|
+
/** Build a failure record from test metadata for the diff report. */
|
|
180
177
|
function captureFailure(
|
|
181
178
|
testCase: TestCase,
|
|
182
179
|
data: ImageSnapshotFailureData,
|
|
183
180
|
message: string,
|
|
184
181
|
): ImageSnapshotFailure {
|
|
185
182
|
const snapshotName = data.actualPath.match(/([^/]+)\.png$/)?.[1] || "unknown";
|
|
186
|
-
|
|
183
|
+
const { mismatchedPixels, mismatchedPixelRatio } = data;
|
|
187
184
|
return {
|
|
188
185
|
testName: testCase.fullName || testCase.name,
|
|
189
186
|
snapshotName,
|
|
190
187
|
comparison: {
|
|
191
188
|
pass: false,
|
|
192
189
|
message,
|
|
193
|
-
mismatchedPixels
|
|
194
|
-
mismatchedPixelRatio
|
|
190
|
+
mismatchedPixels,
|
|
191
|
+
mismatchedPixelRatio,
|
|
195
192
|
},
|
|
196
193
|
paths: {
|
|
197
194
|
reference: data.expectedPath,
|
package/src/SnapshotManager.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
|
|
4
|
+
const replaceableChars = /[<>:"/\\|?*]/g;
|
|
5
|
+
const unreplaceableChars = /\p{Control}+/gu;
|
|
6
|
+
|
|
4
7
|
export interface SnapshotConfig {
|
|
5
8
|
snapshotDir?: string;
|
|
6
9
|
diffDir?: string;
|
|
@@ -29,7 +32,7 @@ export class ImageSnapshotManager {
|
|
|
29
32
|
return path.join(
|
|
30
33
|
this.testDir,
|
|
31
34
|
this.config.snapshotDir,
|
|
32
|
-
`${snapshotName}.png`,
|
|
35
|
+
`${sanitizeName(snapshotName)}.png`,
|
|
33
36
|
);
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -37,12 +40,16 @@ export class ImageSnapshotManager {
|
|
|
37
40
|
return path.join(
|
|
38
41
|
this.testDir,
|
|
39
42
|
this.config.actualDir,
|
|
40
|
-
`${snapshotName}.png`,
|
|
43
|
+
`${sanitizeName(snapshotName)}.png`,
|
|
41
44
|
);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
diffPath(snapshotName: string): string {
|
|
45
|
-
return path.join(
|
|
48
|
+
return path.join(
|
|
49
|
+
this.testDir,
|
|
50
|
+
this.config.diffDir,
|
|
51
|
+
`${sanitizeName(snapshotName)}.png`,
|
|
52
|
+
);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/** Update failing snapshots (only in "all" mode with vitest -u) */
|
|
@@ -88,3 +95,11 @@ export class ImageSnapshotManager {
|
|
|
88
95
|
await fs.promises.writeFile(filepath, buffer);
|
|
89
96
|
}
|
|
90
97
|
}
|
|
98
|
+
|
|
99
|
+
/** Replace chars that are invalid in Windows filenames. */
|
|
100
|
+
function sanitizeName(input: string): string {
|
|
101
|
+
return input
|
|
102
|
+
.replace(replaceableChars, "_")
|
|
103
|
+
.replace(unreplaceableChars, "")
|
|
104
|
+
.trim();
|
|
105
|
+
}
|
|
@@ -17,6 +17,30 @@ test("generates correct file paths", () => {
|
|
|
17
17
|
);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test("sanitizes invalid filename characters", () => {
|
|
21
|
+
const manager = new ImageSnapshotManager(path.join("/path/to/test.ts"));
|
|
22
|
+
const snap = (name: string) => manager.referencePath(name);
|
|
23
|
+
const snapDir = path.join("/path/to", "__image_snapshots__");
|
|
24
|
+
|
|
25
|
+
// filesystem-invalid chars replaced with underscores
|
|
26
|
+
expect(snap("Category > test")).toBe(
|
|
27
|
+
path.join(snapDir, "Category _ test.png"),
|
|
28
|
+
);
|
|
29
|
+
expect(snap('a<>:"/\\|?*b')).toBe(path.join(snapDir, "a_________b.png"));
|
|
30
|
+
|
|
31
|
+
// control chars removed, outer whitespace trimmed
|
|
32
|
+
expect(snap("word1\x02word2")).toBe(path.join(snapDir, "word1word2.png"));
|
|
33
|
+
expect(snap(" spaced ")).toBe(path.join(snapDir, "spaced.png"));
|
|
34
|
+
|
|
35
|
+
// sanitization applied to all path types
|
|
36
|
+
expect(manager.actualPath("a > b")).toBe(
|
|
37
|
+
path.join("/path/to", "__image_actual__", "a _ b.png"),
|
|
38
|
+
);
|
|
39
|
+
expect(manager.diffPath("a > b")).toBe(
|
|
40
|
+
path.join("/path/to", "__image_diffs__", "a _ b.png"),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
20
44
|
test("supports custom directory names", () => {
|
|
21
45
|
const testPath = path.join("/path/to/test.ts");
|
|
22
46
|
const manager = new ImageSnapshotManager(testPath, {
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { Reporter, TestCase, TestSpecification, Vitest } from "vitest/node";
|
|
2
|
-
|
|
3
|
-
//#region src/ImageSnapshotReporter.d.ts
|
|
4
|
-
declare const autoOpenValues: readonly ["always", "failures", "never"];
|
|
5
|
-
type AutoOpen = (typeof autoOpenValues)[number];
|
|
6
|
-
interface ImageSnapshotReporterOptions {
|
|
7
|
-
/** Auto-open report in browser. Default: "failures" or "never" in CI */
|
|
8
|
-
autoOpen?: AutoOpen;
|
|
9
|
-
/** Report directory (relative to config.root or absolute) */
|
|
10
|
-
reportPath?: string;
|
|
11
|
-
/** Port for live-reload server. Set to 0 to disable. Default: 4343 */
|
|
12
|
-
port?: number;
|
|
13
|
-
}
|
|
14
|
-
/** Vitest reporter that generates HTML diff reports for image snapshot failures */
|
|
15
|
-
declare class ImageSnapshotReporter implements Reporter {
|
|
16
|
-
private failuresByFile;
|
|
17
|
-
private vitest;
|
|
18
|
-
private reportPath?;
|
|
19
|
-
private autoOpen;
|
|
20
|
-
private port;
|
|
21
|
-
private serverStarted;
|
|
22
|
-
constructor(options?: ImageSnapshotReporterOptions);
|
|
23
|
-
/** Resolve autoOpen setting with priority: CI override > env var > config option > default */
|
|
24
|
-
private resolveAutoOpen;
|
|
25
|
-
/** Parse and validate IMAGE_DIFF_AUTO_OPEN environment variable */
|
|
26
|
-
private envAutoOpen;
|
|
27
|
-
onInit(vitest: Vitest): void;
|
|
28
|
-
private startServer;
|
|
29
|
-
onTestRunStart(specifications: ReadonlyArray<TestSpecification>): void;
|
|
30
|
-
onTestCaseResult(testCase: TestCase): void;
|
|
31
|
-
onTestRunEnd(): Promise<void>;
|
|
32
|
-
private resolveReportDir;
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
export { ImageSnapshotReporter as n, ImageSnapshotReporterOptions as r, AutoOpen as t };
|