vitest-screenshot 1.0.0 → 1.1.0

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/CHANGELOG.md CHANGED
@@ -1,12 +1,5 @@
1
- ## [vitest-screenshot-v1.0.0](https://github.com/bhouston/vitest-gpu/compare/vitest-screenshot-v0.1.0...vitest-screenshot-v1.0.0) (2026-09-18)
2
-
3
- ### ⚠ BREAKING CHANGES
4
-
5
- * **screenshot:** `threshold` and `maxDiffRatio` are no longer top-level options. Use
6
- `comparatorOptions: { threshold, allowedMismatchedPixelRatio }` (Vitest's names) instead.
7
- * **screenshot:** the matcher is async and must be awaited; reference paths need an image extension.
1
+ ## [vitest-screenshot-v1.1.0](https://github.com/bhouston/vitest-gpu/compare/vitest-screenshot-v1.0.1...vitest-screenshot-v1.1.0) (2026-09-21)
8
2
 
9
3
  ### Features
10
4
 
11
- * **screenshot:** accept canvases, images and any image format in toMatchScreenshot ([edaabc4](https://github.com/bhouston/vitest-gpu/commit/edaabc43ac0ab895601565129a8db38121cfa5d3)), closes [#1](https://github.com/bhouston/vitest-gpu/issues/1)
12
- * **screenshot:** vitest-style comparators with an imagemagick metrics comparator ([fd37a14](https://github.com/bhouston/vitest-gpu/commit/fd37a14651b3281ec3cdeec9cfd8f434a4233570)), closes [#3](https://github.com/bhouston/vitest-gpu/issues/3)
5
+ * **screenshot:** follow snapshot update mode ([1af74a4](https://github.com/bhouston/vitest-gpu/commit/1af74a4a94dec9912d5c5d37169adf3ff1259f73)), closes [#27](https://github.com/bhouston/vitest-gpu/issues/27)
package/README.md CHANGED
@@ -45,8 +45,12 @@ relative to `baselineDir` unless absolute.
45
45
  - Any format sharp reads and writes works: png, jpg, gif, webp, avif, tiff. **We recommend png**: it is lossless,
46
46
  so a baseline written on the first run matches exactly. jpg and webp are re-encoded lossily on write and gif is
47
47
  limited to 256 colours, so loosen the comparator if you use them.
48
- - A missing baseline is written on first run, except when `CI` is set, where it fails.
49
- - `UPDATE_SCREENSHOTS=1 vitest` (or `update: true`) rewrites baselines.
48
+ - Screenshot baselines follow Vitest's snapshot update mode: missing baselines are created locally, fail in CI,
49
+ and `vitest -u` creates or overwrites them.
50
+ - `UPDATE_SCREENSHOTS=1 vitest` remains an alias for updating all screenshot baselines.
51
+ - Per assertion, `update: true` creates or overwrites a baseline and `update: false` prevents both overwriting and
52
+ creating one. This explicit option takes precedence over Vitest's mode and `UPDATE_SCREENSHOTS`.
53
+ - `.not.toMatchScreenshot()` is unsupported and fails without reading pixels or writing baseline files.
50
54
  - On failure `<name>.actual.png` and `<name>.diff.png` are written to `baselineDir` (named after the test when the
51
55
  reference is in memory). Gitignore them.
52
56
 
@@ -60,11 +64,12 @@ comparators, go to `extendMatchers()` (the equivalent of `test.browser.expect.to
60
64
  extendMatchers({
61
65
  comparatorName: 'pixelmatch',
62
66
  comparatorOptions: { threshold: 0.2, allowedMismatchedPixelRatio: 0.01 },
63
- comparators: { 'my-comparator': (reference, actual, { createDiff, ...options }) => ({ pass, diff, message }) },
64
67
  });
65
68
  ```
66
69
 
67
- Per-assertion `comparatorOptions` are merged over the global ones when they name the same comparator.
70
+ Per-assertion `comparatorOptions` are merged over the global ones when they name the same comparator. When an
71
+ assertion omits `comparatorName`, its options may match any registered comparator because the global default selects
72
+ their meaning at runtime. Supplying `comparatorName` checks the options strictly against that comparator.
68
73
 
69
74
  ### `pixelmatch` (default)
70
75
 
@@ -119,6 +124,37 @@ knot.png: AE 760 (fuzz 0), PAE 0.1569, MAE 0.01042, MSE 0.0007709, RMSE 0.02777,
119
124
  A comparator has Vitest's signature: `(reference, actual, options) => { pass, diff, message }`, where `reference`
120
125
  and `actual` are `{ width, height, data }`, `options` carries `comparatorOptions` plus `createDiff`, and `diff`
121
126
  (an RGBA image or `null`) is written as `<name>.diff.png` on failure. Register it under `comparators` and select
122
- it with `comparatorName`.
127
+ it with `comparatorName`. Add its option type to `ComparatorRegistry` so registration and assertions check the
128
+ custom name and options:
129
+
130
+ ```ts
131
+ import { type Comparator, extendMatchers } from 'vitest-screenshot';
132
+
133
+ declare module 'vitest-screenshot' {
134
+ interface ComparatorRegistry {
135
+ 'red-channel': { tolerance?: number };
136
+ }
137
+ }
138
+
139
+ const redChannel: Comparator<{ tolerance?: number }> = (reference, actual, { tolerance = 0 }) => {
140
+ const difference = Math.abs(reference.data[0]! - actual.data[0]!);
141
+ return { pass: difference <= tolerance, diff: null, message: `red channel differs by ${difference}` };
142
+ };
143
+
144
+ extendMatchers({ comparators: { 'red-channel': redChannel } });
145
+
146
+ await expect(image).toMatchScreenshot('baseline.png', {
147
+ comparatorName: 'red-channel',
148
+ comparatorOptions: { tolerance: 2 },
149
+ });
150
+ ```
123
151
 
124
152
  See [`demo/test/screenshots`](../../demo/test/screenshots) for every source, format and comparator combination.
153
+
154
+ ## Author
155
+
156
+ Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
157
+
158
+ ## License
159
+
160
+ MIT
package/dist/index.d.ts CHANGED
@@ -37,26 +37,34 @@ export type PixelmatchOptions = NonNullable<Parameters<typeof pixelmatch>[5]> &
37
37
  /** Number of pixels allowed to differ. With both limits given the stricter wins; with neither, none may differ. */
38
38
  allowedMismatchedPixels?: number;
39
39
  };
40
- /** Which comparator judges the images. `pixelmatch` (default) and `metrics` are built in; others come from `extendMatchers({ comparators })`. */
40
+ /** Comparator names and their options. Augment this interface when registering a custom comparator. */
41
+ export interface ComparatorRegistry {
42
+ pixelmatch: PixelmatchOptions;
43
+ metrics: MetricsOptions;
44
+ }
45
+ type ComparatorName = Extract<keyof ComparatorRegistry, string>;
46
+ type ComparatorOptions<Name extends ComparatorName> = Extract<ComparatorRegistry[Name], object>;
47
+ /** Which registered comparator judges the images. `pixelmatch` is the default. */
41
48
  export type ComparatorSelection = {
42
- comparatorName?: 'pixelmatch';
43
- comparatorOptions?: PixelmatchOptions;
44
- } | {
45
- comparatorName: 'metrics';
46
- comparatorOptions?: MetricsOptions;
47
- } | {
48
- comparatorName: string;
49
- comparatorOptions?: Record<string, unknown>;
49
+ [Name in ComparatorName]: {
50
+ comparatorName: Name;
51
+ comparatorOptions?: ComparatorOptions<Name>;
52
+ };
53
+ }[ComparatorName] | {
54
+ comparatorName?: undefined;
55
+ comparatorOptions?: ComparatorOptions<ComparatorName>;
50
56
  };
51
57
  export type ScreenshotOptions = ComparatorSelection & {
52
58
  /** Directory relative reference paths and diff images live in. Default: `__screenshots__` beside the test file. */
53
59
  baselineDir?: string;
54
- /** Overwrite the baseline file. Default: `UPDATE_SCREENSHOTS` env var is set. */
60
+ /** Override Vitest's snapshot update mode. `true` creates/overwrites; `false` prevents both. */
55
61
  update?: boolean;
56
62
  };
57
63
  /** Defaults for every assertion plus custom comparators, like `test.browser.expect.toMatchScreenshot` in Vitest config. */
58
64
  export type ScreenshotConfig = ComparatorSelection & {
59
- comparators?: Record<string, Comparator<never>>;
65
+ comparators?: {
66
+ [Name in ComparatorName]?: Comparator<ComparatorOptions<Name>>;
67
+ };
60
68
  };
61
69
  declare module 'vitest' {
62
70
  interface Assertion {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEtE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE3D,0HAA0H;AAC1H,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAAA;CAAE,CAAC;AAEhG;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB,SAAS,GACT;IAAE,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAAA;CAAE,GACpC;IAAE,YAAY,IAAI,SAAS,CAAA;CAAE,GAC7B;IAAE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEhC,qHAAqH;AACrH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7C,+GAA+G;AAC/G,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEjG,mFAAmF;AACnF,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACzE,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,OAAO,GAAG;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,KACvC,gBAAgB,CAAC;AAEtB,uFAAuF;AACvF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG;IAC9E,mDAAmD;IACnD,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,mHAAmH;IACnH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,iJAAiJ;AACjJ,MAAM,MAAM,mBAAmB,GAC3B;IAAE,cAAc,CAAC,EAAE,YAAY,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACxE;IAAE,cAAc,EAAE,SAAS,CAAC;IAAC,iBAAiB,CAAC,EAAE,cAAc,CAAA;CAAE,GACjE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG;IACpD,mHAAmH;IACnH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,2HAA2H;AAC3H,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAA;CAAE,CAAC;AAEzG,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,SAAS;QACjB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACrF;CACF;AA+BD,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,EAAE,UAAU,CAAC,iBAAiB,CAgB9D,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,GAAE,gBAAqB,GAAG,IAAI,CAiDlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import pixelmatch from 'pixelmatch';
4
4
  import sharp from 'sharp';
5
5
  import { expect } from 'vitest';
6
6
  import { metricsComparator } from './metrics.js';
7
+ import { resolveScreenshotUpdateState } from './update-policy.js';
7
8
  const result = (pass, message) => ({ pass, message: () => message });
8
9
  const raw = ({ width, height, data }) => sharp(data, { raw: { width, height, channels: 4 } });
9
10
  /** Decodes any format sharp reads (png, jpg, gif, webp, ...) into RGBA8. */
@@ -54,6 +55,8 @@ export function extendMatchers(config = {}) {
54
55
  };
55
56
  expect.extend({
56
57
  async toMatchScreenshot(received, reference, options = {}) {
58
+ if (this.isNot)
59
+ throw new Error('`.not.toMatchScreenshot()` is not supported');
57
60
  const dir = options.baselineDir ?? join(dirname(this.testPath), '__screenshots__');
58
61
  const actual = await toRgba(received);
59
62
  let file;
@@ -62,13 +65,14 @@ export function extendMatchers(config = {}) {
62
65
  if (!extname(reference))
63
66
  throw new Error(`reference "${reference}" needs an image extension, e.g. "${reference}.png"`);
64
67
  file = isAbsolute(reference) ? reference : join(dir, reference);
65
- const update = options.update ?? Boolean(process.env.UPDATE_SCREENSHOTS);
66
- if (update || (!existsSync(file) && !process.env.CI)) {
68
+ const exists = existsSync(file);
69
+ const update = resolveScreenshotUpdateState(this.snapshotState, options.update, Boolean(process.env.UPDATE_SCREENSHOTS), Boolean(process.env.CI));
70
+ if (update === 'all' || (update === 'new' && !exists)) {
67
71
  await writeImage(file, actual);
68
72
  return result(true, `wrote baseline ${file}`);
69
73
  }
70
- if (!existsSync(file))
71
- return result(false, `missing baseline ${file}; run with UPDATE_SCREENSHOTS=1`);
74
+ if (!exists)
75
+ return result(false, `missing baseline ${file}; run with UPDATE_SCREENSHOTS=1 or vitest -u`);
72
76
  baseline = await readImage(file);
73
77
  }
74
78
  else {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type ScreenshotUpdateState = 'all' | 'new' | 'none';
2
+ export declare const resolveScreenshotUpdateState: (snapshotState: unknown, explicitUpdate: boolean | undefined, environmentUpdate: boolean, isCi: boolean) => ScreenshotUpdateState;
3
+ //# sourceMappingURL=update-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-policy.d.ts","sourceRoot":"","sources":["../src/update-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAW3D,eAAO,MAAM,4BAA4B,kBACxB,OAAO,kBACN,OAAO,GAAG,SAAS,qBAChB,OAAO,QACpB,OAAO,KACZ,qBAMF,CAAC"}
@@ -0,0 +1,11 @@
1
+ const isUpdateState = (value) => value === 'all' || value === 'new' || value === 'none';
2
+ export const resolveScreenshotUpdateState = (snapshotState, explicitUpdate, environmentUpdate, isCi) => {
3
+ if (explicitUpdate !== undefined)
4
+ return explicitUpdate ? 'all' : 'none';
5
+ if (environmentUpdate)
6
+ return 'all';
7
+ const compatible = snapshotState;
8
+ const state = compatible?.snapshotUpdateState ?? compatible?._updateSnapshot;
9
+ return isUpdateState(state) ? state : isCi ? 'none' : 'new';
10
+ };
11
+ //# sourceMappingURL=update-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-policy.js","sourceRoot":"","sources":["../src/update-policy.ts"],"names":[],"mappings":"AAQA,MAAM,aAAa,GAAG,CAAC,KAAc,EAAkC,EAAE,CACvE,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC;AAEzD,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAC1C,aAAsB,EACtB,cAAmC,EACnC,iBAA0B,EAC1B,IAAa,EACU,EAAE;IACzB,IAAI,cAAc,KAAK,SAAS;QAAE,OAAO,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;IACzE,IAAI,iBAAiB;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,UAAU,GAAG,aAAoD,CAAC;IACxE,MAAM,KAAK,GAAG,UAAU,EAAE,mBAAmB,IAAI,UAAU,EAAE,eAAe,CAAC;IAC7E,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;AAC9D,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-screenshot",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "toMatchScreenshot(): pixel-diff a canvas, Image or ImageData against a committed png/jpg/gif/webp baseline in Vitest",
5
5
  "keywords": [
6
6
  "matcher",
@@ -27,6 +27,8 @@
27
27
  "dist/**/*.js.map",
28
28
  "dist/**/*.d.ts",
29
29
  "dist/**/*.d.ts.map",
30
+ "src/**/*.ts",
31
+ "!src/**/*.test.ts",
30
32
  "CHANGELOG.md"
31
33
  ],
32
34
  "type": "module",
@@ -43,7 +45,7 @@
43
45
  },
44
46
  "scripts": {
45
47
  "build": "tsc",
46
- "tsc": "tsc"
48
+ "tsc": "tsc && tsc -p tsconfig.type-tests.json"
47
49
  },
48
50
  "dependencies": {
49
51
  "pixelmatch": "^7.1.0",
package/src/index.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { basename, dirname, extname, isAbsolute, join } from 'node:path';
3
+ import pixelmatch from 'pixelmatch';
4
+ import sharp, { type FormatEnum } from 'sharp';
5
+ import { expect } from 'vitest';
6
+ import { metricsComparator, type MetricsOptions } from './metrics.js';
7
+ import { resolveScreenshotUpdateState } from './update-policy.js';
8
+
9
+ export type { Metric, MetricsOptions } from './metrics.js';
10
+
11
+ /** Anything with RGBA8 pixels, top row first: `ImageData`, node-webgl's `canvas.getImageData()`, or a WebGPU readback. */
12
+ export type RgbaImage = { width: number; height: number; data: Uint8Array | Uint8ClampedArray };
13
+
14
+ /**
15
+ * A source of pixels: an `RgbaImage` / `ImageData`, a WebGPU `HeadlessCanvas` (`readPixels()`), a node-webgl
16
+ * `Canvas` (`getImageData()`), or a node-webgl `Image` (decoded with `decode()`).
17
+ */
18
+ export type ImageSource =
19
+ | RgbaImage
20
+ | { readPixels(): Promise<RgbaImage> }
21
+ | { getImageData(): RgbaImage }
22
+ | { decode(): Promise<void> };
23
+
24
+ /** A baseline: a file path with extension (relative to `baselineDir` or absolute), or an in-memory `ImageSource`. */
25
+ export type Reference = string | ImageSource;
26
+
27
+ /** What a comparator returns, as in Vitest browser mode: `diff` is written next to the baseline on failure. */
28
+ export type ComparatorResult = { pass: boolean; diff: RgbaImage | null; message: string | null };
29
+
30
+ /** A comparator, with the signature Vitest browser mode uses for `comparators`. */
31
+ export type Comparator<Options extends object = Record<string, unknown>> = (
32
+ reference: RgbaImage,
33
+ actual: RgbaImage,
34
+ options: Options & { createDiff: boolean },
35
+ ) => ComparatorResult;
36
+
37
+ /** Options of the default `pixelmatch` comparator, named as in Vitest browser mode. */
38
+ export type PixelmatchOptions = NonNullable<Parameters<typeof pixelmatch>[5]> & {
39
+ /** Fraction (0..1) of pixels allowed to differ. */
40
+ allowedMismatchedPixelRatio?: number;
41
+ /** Number of pixels allowed to differ. With both limits given the stricter wins; with neither, none may differ. */
42
+ allowedMismatchedPixels?: number;
43
+ };
44
+
45
+ /** Comparator names and their options. Augment this interface when registering a custom comparator. */
46
+ export interface ComparatorRegistry {
47
+ pixelmatch: PixelmatchOptions;
48
+ metrics: MetricsOptions;
49
+ }
50
+
51
+ type ComparatorName = Extract<keyof ComparatorRegistry, string>;
52
+ type ComparatorOptions<Name extends ComparatorName> = Extract<ComparatorRegistry[Name], object>;
53
+
54
+ /** Which registered comparator judges the images. `pixelmatch` is the default. */
55
+ export type ComparatorSelection =
56
+ | {
57
+ [Name in ComparatorName]: {
58
+ comparatorName: Name;
59
+ comparatorOptions?: ComparatorOptions<Name>;
60
+ };
61
+ }[ComparatorName]
62
+ | { comparatorName?: undefined; comparatorOptions?: ComparatorOptions<ComparatorName> };
63
+
64
+ export type ScreenshotOptions = ComparatorSelection & {
65
+ /** Directory relative reference paths and diff images live in. Default: `__screenshots__` beside the test file. */
66
+ baselineDir?: string;
67
+ /** Override Vitest's snapshot update mode. `true` creates/overwrites; `false` prevents both. */
68
+ update?: boolean;
69
+ };
70
+
71
+ /** Defaults for every assertion plus custom comparators, like `test.browser.expect.toMatchScreenshot` in Vitest config. */
72
+ export type ScreenshotConfig = ComparatorSelection & {
73
+ comparators?: { [Name in ComparatorName]?: Comparator<ComparatorOptions<Name>> };
74
+ };
75
+
76
+ declare module 'vitest' {
77
+ interface Assertion {
78
+ toMatchScreenshot(reference: Reference, options?: ScreenshotOptions): Promise<void>;
79
+ }
80
+ }
81
+
82
+ const result = (pass: boolean, message: string) => ({ pass, message: () => message });
83
+
84
+ const raw = ({ width, height, data }: RgbaImage) => sharp(data, { raw: { width, height, channels: 4 } });
85
+
86
+ /** Decodes any format sharp reads (png, jpg, gif, webp, ...) into RGBA8. */
87
+ const readImage = async (file: string): Promise<RgbaImage> => {
88
+ const { data, info } = await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
89
+ return { width: info.width, height: info.height, data };
90
+ };
91
+
92
+ /** Encodes by file extension; lossy formats (jpg, webp) alter pixels on write. */
93
+ const writeImage = (file: string, image: RgbaImage) => {
94
+ mkdirSync(dirname(file), { recursive: true });
95
+ return raw(image)
96
+ .toFormat(extname(file).slice(1) as keyof FormatEnum)
97
+ .toFile(file);
98
+ };
99
+
100
+ const toRgba = async (source: ImageSource): Promise<RgbaImage> => {
101
+ if ('data' in source) return source;
102
+ if ('readPixels' in source) return source.readPixels();
103
+ if ('getImageData' in source) return source.getImageData();
104
+ await source.decode();
105
+ // node-webgl's Image and ImageBitmap expose decoded pixels this way (it is what their own texImage2D uses).
106
+ const rgba = (source as { _toRGBA8?: () => RgbaImage })._toRGBA8?.();
107
+ if (!rgba) throw new Error('unsupported image source: expected RGBA data, readPixels(), getImageData() or an Image');
108
+ return rgba;
109
+ };
110
+
111
+ /** `comparatorName: 'pixelmatch'` (default): Vitest browser mode's built-in comparison. */
112
+ export const pixelmatchComparator: Comparator<PixelmatchOptions> = (
113
+ reference,
114
+ actual,
115
+ { createDiff, allowedMismatchedPixelRatio, allowedMismatchedPixels, ...pixelmatchOptions },
116
+ ) => {
117
+ const { width, height } = actual;
118
+ const total = width * height;
119
+ const diff = createDiff ? new Uint8Array(total * 4) : undefined;
120
+ const count = pixelmatch(reference.data, actual.data, diff, width, height, pixelmatchOptions);
121
+ const limits = [allowedMismatchedPixels, allowedMismatchedPixelRatio && allowedMismatchedPixelRatio * total];
122
+ const allowed = limits.some((l) => l !== undefined) ? Math.min(...limits.filter((l) => l !== undefined)) : 0;
123
+ return {
124
+ pass: count <= allowed,
125
+ diff: diff ? { width, height, data: diff } : null,
126
+ message: `${count} pixels (${((count / total) * 100).toFixed(3)}%) differ, ${Math.floor(allowed)} allowed`,
127
+ };
128
+ };
129
+
130
+ export function extendMatchers(config: ScreenshotConfig = {}): void {
131
+ const comparators = {
132
+ pixelmatch: pixelmatchComparator,
133
+ metrics: metricsComparator,
134
+ ...config.comparators,
135
+ } as unknown as Record<string, Comparator>;
136
+ expect.extend({
137
+ async toMatchScreenshot(received: ImageSource, reference: Reference, options: ScreenshotOptions = {}) {
138
+ if (this.isNot) throw new Error('`.not.toMatchScreenshot()` is not supported');
139
+ const dir = options.baselineDir ?? join(dirname(this.testPath!), '__screenshots__');
140
+ const actual = await toRgba(received);
141
+ let file: string | undefined;
142
+ let baseline: RgbaImage;
143
+ if (typeof reference === 'string') {
144
+ if (!extname(reference))
145
+ throw new Error(`reference "${reference}" needs an image extension, e.g. "${reference}.png"`);
146
+ file = isAbsolute(reference) ? reference : join(dir, reference);
147
+ const exists = existsSync(file);
148
+ const update = resolveScreenshotUpdateState(
149
+ this.snapshotState,
150
+ options.update,
151
+ Boolean(process.env.UPDATE_SCREENSHOTS),
152
+ Boolean(process.env.CI),
153
+ );
154
+ if (update === 'all' || (update === 'new' && !exists)) {
155
+ await writeImage(file, actual);
156
+ return result(true, `wrote baseline ${file}`);
157
+ }
158
+ if (!exists) return result(false, `missing baseline ${file}; run with UPDATE_SCREENSHOTS=1 or vitest -u`);
159
+ baseline = await readImage(file);
160
+ } else {
161
+ baseline = await toRgba(reference);
162
+ }
163
+ const label = file ? basename(file) : this.currentTestName!;
164
+ if (baseline.width !== actual.width || baseline.height !== actual.height) {
165
+ return result(
166
+ false,
167
+ `${label}: size mismatch: baseline ${baseline.width}x${baseline.height}, actual ${actual.width}x${actual.height}`,
168
+ );
169
+ }
170
+ const name = options.comparatorName ?? config.comparatorName ?? 'pixelmatch';
171
+ const comparator = comparators[name];
172
+ if (!comparator) throw new Error(`unknown comparator "${name}"; known: ${Object.keys(comparators).join(', ')}`);
173
+ const inherited = name === (config.comparatorName ?? 'pixelmatch') ? config.comparatorOptions : undefined;
174
+ const { pass, diff, message } = comparator(baseline, actual, {
175
+ ...inherited,
176
+ ...options.comparatorOptions,
177
+ createDiff: true,
178
+ });
179
+ const stem = join(dir, file ? basename(file, extname(file)) : label.replace(/[^\w-]+/g, '_'));
180
+ const seeDiff = !pass && diff ? `; see ${stem}.diff.png` : '';
181
+ if (!pass)
182
+ await Promise.all([writeImage(`${stem}.actual.png`, actual), diff && writeImage(`${stem}.diff.png`, diff)]);
183
+ return result(pass, `${label}: ${message ?? (pass ? 'matches' : 'differs')}${seeDiff}`);
184
+ },
185
+ });
186
+ }
package/src/metrics.ts ADDED
@@ -0,0 +1,187 @@
1
+ import type { Comparator, RgbaImage } from './index.js';
2
+
3
+ /** ImageMagick `compare -metric` names. Upper bounds: AE PAE MAE MSE RMSE DSSIM. Lower bounds: PSNR SSIM. */
4
+ export type Metric = 'AE' | 'PAE' | 'MAE' | 'MSE' | 'RMSE' | 'PSNR' | 'SSIM' | 'DSSIM';
5
+
6
+ export type MetricsOptions = {
7
+ /** ImageMagick `-fuzz`: per-channel RMS colour distance (0..1) within which two pixels count as equal. Default 0. */
8
+ fuzz?: number;
9
+ /** Absolute error: pixels differing by more than `fuzz`. Default bound when no metric is given: 0. */
10
+ AE?: number;
11
+ /** Peak absolute error, 0..1. */
12
+ PAE?: number;
13
+ /** Mean absolute error, 0..1. */
14
+ MAE?: number;
15
+ /** Mean squared error, 0..1. */
16
+ MSE?: number;
17
+ /** Root mean squared error, 0..1. */
18
+ RMSE?: number;
19
+ /** Peak signal-to-noise ratio in dB (lower bound). `Infinity` for identical images. */
20
+ PSNR?: number;
21
+ /** Structural similarity, 1 = identical (lower bound). Gaussian 11x11 window, sigma 1.5, over RGB. */
22
+ SSIM?: number;
23
+ /** Structural dissimilarity, `(1 - SSIM) / 2`. */
24
+ DSSIM?: number;
25
+ };
26
+
27
+ const LOWER_IS_WORSE = new Set<Metric>(['PSNR', 'SSIM']);
28
+ const METRICS = new Set<Metric>(['AE', 'PAE', 'MAE', 'MSE', 'RMSE', 'PSNR', 'SSIM', 'DSSIM']);
29
+
30
+ /**
31
+ * ImageMagick's IsFuzzyEquivalencePixel: alpha must be within `fuzz`, then the colour distance, scaled by both
32
+ * alphas (a 4D cone: transparent pixels always match), must be within `fuzz` per channel RMS.
33
+ */
34
+ const fuzzyEqual = (a: RgbaImage['data'], b: RgbaImage['data'], i: number, fuzz2: number) => {
35
+ const sa = a[i + 3]! / 255;
36
+ const sb = b[i + 3]! / 255;
37
+ const dAlpha = (sa - sb) ** 2;
38
+ if (dAlpha > fuzz2) return false;
39
+ const scale = sa * sb;
40
+ if (scale <= Number.EPSILON) return true;
41
+ let d = 3 * dAlpha;
42
+ for (let c = 0; c < 3; c++) d += scale * ((a[i + c]! - b[i + c]!) / 255) ** 2;
43
+ return d <= 3 * fuzz2;
44
+ };
45
+
46
+ /**
47
+ * AE, PAE, MAE, MSE, RMSE and PSNR in one pass, matching `magick compare` for RGBA images: colour channels are
48
+ * alpha-premultiplied and all four channels are averaged. Also returns which pixels differ by more than `fuzz`.
49
+ */
50
+ export function distortion(a: RgbaImage, b: RgbaImage, fuzz: number) {
51
+ const n = a.width * a.height;
52
+ const mask = new Uint8Array(n);
53
+ const fuzz2 = fuzz * fuzz;
54
+ let AE = 0;
55
+ let PAE = 0;
56
+ let sumAbs = 0;
57
+ let sumSq = 0;
58
+ for (let p = 0, i = 0; p < n; p++, i += 4) {
59
+ if (!fuzzyEqual(a.data, b.data, i, fuzz2)) {
60
+ mask[p] = 1;
61
+ AE++;
62
+ }
63
+ const sa = a.data[i + 3]! / 255;
64
+ const sb = b.data[i + 3]! / 255;
65
+ for (let c = 0; c < 4; c++) {
66
+ const d = c === 3 ? Math.abs(sa - sb) : Math.abs((a.data[i + c]! / 255) * sa - (b.data[i + c]! / 255) * sb);
67
+ if (d > PAE) PAE = d;
68
+ sumAbs += d;
69
+ sumSq += d * d;
70
+ }
71
+ }
72
+ const MSE = sumSq / (n * 4);
73
+ return {
74
+ AE,
75
+ PAE,
76
+ MAE: sumAbs / (n * 4),
77
+ MSE,
78
+ RMSE: Math.sqrt(MSE),
79
+ PSNR: MSE > 0 ? 10 * Math.log10(1 / MSE) : Infinity,
80
+ mask,
81
+ };
82
+ }
83
+
84
+ /** Gaussian taps for an 11x11 window with sigma 1.5, normalised to sum 1 (Wang et al. 2004). */
85
+ const KERNEL = (() => {
86
+ const k = Float64Array.from({ length: 11 }, (_, i) => Math.exp(-((i - 5) ** 2) / (2 * 1.5 * 1.5)));
87
+ const sum = k.reduce((s, v) => s + v, 0);
88
+ return k.map((v) => v / sum);
89
+ })();
90
+
91
+ /** Separable Gaussian blur of a 3-channel float image, clamping to edge pixels. */
92
+ const blur = (src: Float32Array, width: number, height: number) => {
93
+ const pass = (input: Float32Array, dx: number, dy: number) => {
94
+ const out = new Float32Array(input.length);
95
+ for (let y = 0; y < height; y++)
96
+ for (let x = 0; x < width; x++)
97
+ for (let c = 0; c < 3; c++) {
98
+ let s = 0;
99
+ for (let k = -5; k <= 5; k++) {
100
+ const sx = Math.min(width - 1, Math.max(0, x + k * dx));
101
+ const sy = Math.min(height - 1, Math.max(0, y + k * dy));
102
+ s += KERNEL[k + 5]! * input[(sy * width + sx) * 3 + c]!;
103
+ }
104
+ out[(y * width + x) * 3 + c] = s;
105
+ }
106
+ return out;
107
+ };
108
+ return pass(pass(src, 1, 0), 0, 1);
109
+ };
110
+
111
+ const mul = (x: Float32Array, y: Float32Array) => x.map((v, i) => v * y[i]!);
112
+
113
+ /** Mean SSIM over RGB (K1 0.01, K2 0.03), the textbook definition also used by ssim.js and Playwright. */
114
+ export function ssim(a: RgbaImage, b: RgbaImage): number {
115
+ const { width, height } = a;
116
+ const rgb = (img: RgbaImage) =>
117
+ Float32Array.from({ length: width * height * 3 }, (_, i) => img.data[i + Math.floor(i / 3)]! / 255);
118
+ const fa = rgb(a);
119
+ const fb = rgb(b);
120
+ const [ma, mb, maa, mbb, mab] = [fa, fb, mul(fa, fa), mul(fb, fb), mul(fa, fb)].map((img) =>
121
+ blur(img, width, height),
122
+ ) as Float32Array[];
123
+ const c1 = 0.01 ** 2;
124
+ const c2 = 0.03 ** 2;
125
+ let sum = 0;
126
+ for (let i = 0; i < fa.length; i++) {
127
+ const va = maa![i]! - ma![i]! ** 2;
128
+ const vb = mbb![i]! - mb![i]! ** 2;
129
+ const cov = mab![i]! - ma![i]! * mb![i]!;
130
+ sum += ((2 * ma![i]! * mb![i]! + c1) * (2 * cov + c2)) / ((ma![i]! ** 2 + mb![i]! ** 2 + c1) * (va + vb + c2));
131
+ }
132
+ return sum / fa.length;
133
+ }
134
+
135
+ /** Diff image in pixelmatch's style: the actual image faded towards white, differing pixels in red. */
136
+ const diffImage = (actual: RgbaImage, mask: Uint8Array): RgbaImage => {
137
+ const data = new Uint8Array(actual.data.length);
138
+ for (let p = 0, i = 0; p < mask.length; p++, i += 4) {
139
+ if (mask[p]) data.set([255, 0, 0, 255], i);
140
+ else {
141
+ const grey =
142
+ 255 - 0.1 * (255 - (0.299 * actual.data[i]! + 0.587 * actual.data[i + 1]! + 0.114 * actual.data[i + 2]!));
143
+ data.set([grey, grey, grey, 255], i);
144
+ }
145
+ }
146
+ return { width: actual.width, height: actual.height, data };
147
+ };
148
+
149
+ const fmt = (n: number) => (Number.isFinite(n) ? String(Number(n.toPrecision(4))) : String(n));
150
+
151
+ /**
152
+ * `comparatorName: 'metrics'`: ImageMagick `compare -metric` bounds. Give any subset; all must pass. With no
153
+ * bound given, requires `AE: 0` (an exact match, like `compare -metric AE`).
154
+ */
155
+ export const metricsComparator: Comparator<MetricsOptions> = (
156
+ reference,
157
+ actual,
158
+ { createDiff, fuzz = 0, ...bounds },
159
+ ) => {
160
+ for (const key of Object.keys(bounds)) {
161
+ if (!METRICS.has(key as Metric))
162
+ throw new Error(`unknown metric "${key}"; expected one of ${[...METRICS].join(', ')}`);
163
+ }
164
+ const wanted = (Object.keys(bounds) as Metric[]).filter((k) => bounds[k] !== undefined);
165
+ if (wanted.length === 0) {
166
+ bounds.AE = 0;
167
+ wanted.push('AE');
168
+ }
169
+ const values: Partial<Record<Metric, number>> & ReturnType<typeof distortion> = distortion(reference, actual, fuzz);
170
+ if (wanted.includes('SSIM') || wanted.includes('DSSIM')) {
171
+ values.SSIM = ssim(reference, actual);
172
+ values.DSSIM = (1 - values.SSIM) / 2;
173
+ }
174
+ const failed = wanted.filter((k) => (LOWER_IS_WORSE.has(k) ? values[k]! < bounds[k]! : values[k]! > bounds[k]!));
175
+ const report = [...METRICS]
176
+ .filter((k) => values[k] !== undefined)
177
+ .map((k) => `${k} ${fmt(values[k]!)}${k === 'AE' ? ` (fuzz ${fmt(fuzz)})` : ''}`)
178
+ .join(', ');
179
+ const why = failed
180
+ .map((k) => `${k} ${fmt(values[k]!)} ${LOWER_IS_WORSE.has(k) ? '<' : '>'} ${fmt(bounds[k]!)}`)
181
+ .join(', ');
182
+ return {
183
+ pass: failed.length === 0,
184
+ diff: createDiff ? diffImage(actual, values.mask) : null,
185
+ message: failed.length ? `${report}; failed: ${why}` : report,
186
+ };
187
+ };
@@ -0,0 +1,23 @@
1
+ export type ScreenshotUpdateState = 'all' | 'new' | 'none';
2
+
3
+ type CompatibleSnapshotState = {
4
+ readonly snapshotUpdateState?: unknown;
5
+ /** Vitest 3 compatibility; remove when the peer range no longer includes Vitest 3. */
6
+ readonly _updateSnapshot?: unknown;
7
+ };
8
+
9
+ const isUpdateState = (value: unknown): value is ScreenshotUpdateState =>
10
+ value === 'all' || value === 'new' || value === 'none';
11
+
12
+ export const resolveScreenshotUpdateState = (
13
+ snapshotState: unknown,
14
+ explicitUpdate: boolean | undefined,
15
+ environmentUpdate: boolean,
16
+ isCi: boolean,
17
+ ): ScreenshotUpdateState => {
18
+ if (explicitUpdate !== undefined) return explicitUpdate ? 'all' : 'none';
19
+ if (environmentUpdate) return 'all';
20
+ const compatible = snapshotState as CompatibleSnapshotState | undefined;
21
+ const state = compatible?.snapshotUpdateState ?? compatible?._updateSnapshot;
22
+ return isUpdateState(state) ? state : isCi ? 'none' : 'new';
23
+ };