vitest-screenshot 0.1.0 → 1.0.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 +12 -0
- package/README.md +103 -11
- package/dist/index.d.ts +55 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -31
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +44 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +150 -0
- package/dist/metrics.js.map +1 -0
- package/package.json +5 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
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)
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# vitest-screenshot
|
|
2
2
|
|
|
3
|
-
`expect(image).toMatchScreenshot(
|
|
4
|
-
|
|
5
|
-
[`vitest-environment-webgl-node`](../vitest-environment-webgl-node)
|
|
6
|
-
|
|
3
|
+
`await expect(image).toMatchScreenshot(reference)`: compare an image against a baseline committed next to your
|
|
4
|
+
tests, with the same options as Vitest browser mode's `toMatchScreenshot`. Backend-agnostic: feed it a canvas from
|
|
5
|
+
[`vitest-environment-webgl-node`](../vitest-environment-webgl-node) or
|
|
6
|
+
[`vitest-environment-webgpu-node`](../vitest-environment-webgpu-node), an `ImageData`, a WebGPU texture
|
|
7
|
+
readback, or pixels from a real browser.
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -17,16 +18,107 @@ pnpm add -D vitest vitest-screenshot
|
|
|
17
18
|
import { extendMatchers } from 'vitest-screenshot';
|
|
18
19
|
extendMatchers();
|
|
19
20
|
|
|
20
|
-
it('renders the knot', () => {
|
|
21
|
+
it('renders the knot', async () => {
|
|
21
22
|
renderer.render(scene, camera);
|
|
22
|
-
expect(canvas
|
|
23
|
+
await expect(canvas).toMatchScreenshot('knot.png', {
|
|
24
|
+
comparatorOptions: { allowedMismatchedPixelRatio: 0.01 },
|
|
25
|
+
});
|
|
23
26
|
});
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
The matcher is async: always `await` it.
|
|
30
|
+
|
|
31
|
+
### What you can compare
|
|
32
|
+
|
|
33
|
+
The received value and the reference can each be any of:
|
|
34
|
+
|
|
35
|
+
- `{ width, height, data }` with RGBA8 pixels, top row first (an `ImageData`, `canvas.getImageData()`, a WebGPU readback).
|
|
36
|
+
- A canvas: node-webgl's `Canvas` (read with `getImageData()`) or the WebGPU environment's `HeadlessCanvas` (read with `readPixels()`).
|
|
37
|
+
- An `Image` from `vitest-environment-webgl-node` (decoded with `decode()` first).
|
|
38
|
+
|
|
39
|
+
The reference may also be a file path. It must include the extension (`'knot.png'`, not `'knot'`), and resolves
|
|
40
|
+
relative to `baselineDir` unless absolute.
|
|
41
|
+
|
|
42
|
+
### Baseline files
|
|
43
|
+
|
|
44
|
+
- Baselines live in `__screenshots__/` beside the test file (override with `baselineDir`).
|
|
45
|
+
- Any format sharp reads and writes works: png, jpg, gif, webp, avif, tiff. **We recommend png**: it is lossless,
|
|
46
|
+
so a baseline written on the first run matches exactly. jpg and webp are re-encoded lossily on write and gif is
|
|
47
|
+
limited to 256 colours, so loosen the comparator if you use them.
|
|
27
48
|
- A missing baseline is written on first run, except when `CI` is set, where it fails.
|
|
28
49
|
- `UPDATE_SCREENSHOTS=1 vitest` (or `update: true`) rewrites baselines.
|
|
29
|
-
- On failure `<name>.actual.png` and `<name>.diff.png` are written
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
- On failure `<name>.actual.png` and `<name>.diff.png` are written to `baselineDir` (named after the test when the
|
|
51
|
+
reference is in memory). Gitignore them.
|
|
52
|
+
|
|
53
|
+
## Comparators
|
|
54
|
+
|
|
55
|
+
As in [Vitest browser mode](https://vitest.dev/guide/browser/visual-regression-testing), `comparatorName` picks
|
|
56
|
+
how the two images are judged and `comparatorOptions` configures it. Defaults for every assertion, and custom
|
|
57
|
+
comparators, go to `extendMatchers()` (the equivalent of `test.browser.expect.toMatchScreenshot` in Vitest config):
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
extendMatchers({
|
|
61
|
+
comparatorName: 'pixelmatch',
|
|
62
|
+
comparatorOptions: { threshold: 0.2, allowedMismatchedPixelRatio: 0.01 },
|
|
63
|
+
comparators: { 'my-comparator': (reference, actual, { createDiff, ...options }) => ({ pass, diff, message }) },
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Per-assertion `comparatorOptions` are merged over the global ones when they name the same comparator.
|
|
68
|
+
|
|
69
|
+
### `pixelmatch` (default)
|
|
70
|
+
|
|
71
|
+
Vitest's built-in comparison: counts pixels whose perceptual colour distance exceeds `threshold`.
|
|
72
|
+
|
|
73
|
+
| Option | Default | Meaning |
|
|
74
|
+
| ------------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------- |
|
|
75
|
+
| `threshold` | `0.1` | Per-pixel colour distance (0..1) tolerated; smaller is stricter. |
|
|
76
|
+
| `allowedMismatchedPixelRatio` | | Fraction (0..1) of pixels allowed to differ. |
|
|
77
|
+
| `allowedMismatchedPixels` | | Number of pixels allowed to differ. |
|
|
78
|
+
| `includeAA`, `alpha`, `aaColor`, `diffColor`, `diffColorAlt`, `diffMask` | pixelmatch defaults | Forwarded to [pixelmatch](https://github.com/mapbox/pixelmatch). |
|
|
79
|
+
|
|
80
|
+
With both limits given the stricter wins; with neither, no pixel may differ. GPUs and drivers rasterize edges
|
|
81
|
+
slightly differently, so set a ratio for baselines shared across machines.
|
|
82
|
+
|
|
83
|
+
### `metrics`
|
|
84
|
+
|
|
85
|
+
Whole-image error bounds with the names and semantics of ImageMagick's `compare -metric`. Give any subset; the
|
|
86
|
+
assertion passes only if every bound holds. With none given it requires an exact match (`AE: 0`), like
|
|
87
|
+
`compare -metric AE`.
|
|
88
|
+
|
|
89
|
+
| Option | Bound | Unit | Meaning |
|
|
90
|
+
| ------- | ----- | ------ | ------------------------------------------------------------------------------------------------------------------ |
|
|
91
|
+
| `fuzz` | | 0..1 | `-fuzz`: colour distance within which two pixels count as equal (default 0). Only affects `AE` and the diff image. |
|
|
92
|
+
| `AE` | max | pixels | Absolute error: pixels differing by more than `fuzz`. |
|
|
93
|
+
| `PAE` | max | 0..1 | Peak absolute error: the largest channel difference anywhere. |
|
|
94
|
+
| `MAE` | max | 0..1 | Mean absolute error. |
|
|
95
|
+
| `MSE` | max | 0..1 | Mean squared error. |
|
|
96
|
+
| `RMSE` | max | 0..1 | Root mean squared error. |
|
|
97
|
+
| `PSNR` | min | dB | Peak signal-to-noise ratio; `Infinity` for identical images. 30–50 dB is typical for "same picture". |
|
|
98
|
+
| `SSIM` | min | 0..1 | Structural similarity (Gaussian 11×11 window, σ 1.5, over RGB); 1 is identical. |
|
|
99
|
+
| `DSSIM` | max | 0..1 | Structural dissimilarity, `(1 − SSIM) / 2`. |
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await expect(canvas).toMatchScreenshot('knot.png', {
|
|
103
|
+
comparatorName: 'metrics',
|
|
104
|
+
comparatorOptions: { fuzz: 0.05, AE: 50, PSNR: 40 }, // few pixels off by >5%, and low overall error
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`AE`, `PAE`, `MAE`, `MSE`, `RMSE` and `PSNR` are computed as ImageMagick does for images with an alpha channel
|
|
109
|
+
(colours alpha-premultiplied, all four channels averaged) and agree with `magick compare` to the printed digits.
|
|
110
|
+
`SSIM` is the textbook definition over RGB, the value `magick compare -metric SSIM` on an opaque image reports
|
|
111
|
+
as `(1 − SSIM) / 2`. The failure message lists every computed metric so you can pick a bound from a real run:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
knot.png: AE 760 (fuzz 0), PAE 0.1569, MAE 0.01042, MSE 0.0007709, RMSE 0.02777, PSNR 31.13; failed: AE 760 > 0; see __screenshots__/knot.diff.png
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Custom comparators
|
|
118
|
+
|
|
119
|
+
A comparator has Vitest's signature: `(reference, actual, options) => { pass, diff, message }`, where `reference`
|
|
120
|
+
and `actual` are `{ width, height, data }`, `options` carries `comparatorOptions` plus `createDiff`, and `diff`
|
|
121
|
+
(an RGBA image or `null`) is written as `<name>.diff.png` on failure. Register it under `comparators` and select
|
|
122
|
+
it with `comparatorName`.
|
|
123
|
+
|
|
124
|
+
See [`demo/test/screenshots`](../../demo/test/screenshots) for every source, format and comparator combination.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,23 +1,69 @@
|
|
|
1
|
+
import pixelmatch from 'pixelmatch';
|
|
2
|
+
import { type MetricsOptions } from './metrics.js';
|
|
3
|
+
export type { Metric, MetricsOptions } from './metrics.js';
|
|
1
4
|
/** Anything with RGBA8 pixels, top row first: `ImageData`, node-webgl's `canvas.getImageData()`, or a WebGPU readback. */
|
|
2
5
|
export type RgbaImage = {
|
|
3
6
|
width: number;
|
|
4
7
|
height: number;
|
|
5
8
|
data: Uint8Array | Uint8ClampedArray;
|
|
6
9
|
};
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A source of pixels: an `RgbaImage` / `ImageData`, a WebGPU `HeadlessCanvas` (`readPixels()`), a node-webgl
|
|
12
|
+
* `Canvas` (`getImageData()`), or a node-webgl `Image` (decoded with `decode()`).
|
|
13
|
+
*/
|
|
14
|
+
export type ImageSource = RgbaImage | {
|
|
15
|
+
readPixels(): Promise<RgbaImage>;
|
|
16
|
+
} | {
|
|
17
|
+
getImageData(): RgbaImage;
|
|
18
|
+
} | {
|
|
19
|
+
decode(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
/** A baseline: a file path with extension (relative to `baselineDir` or absolute), or an in-memory `ImageSource`. */
|
|
22
|
+
export type Reference = string | ImageSource;
|
|
23
|
+
/** What a comparator returns, as in Vitest browser mode: `diff` is written next to the baseline on failure. */
|
|
24
|
+
export type ComparatorResult = {
|
|
25
|
+
pass: boolean;
|
|
26
|
+
diff: RgbaImage | null;
|
|
27
|
+
message: string | null;
|
|
28
|
+
};
|
|
29
|
+
/** A comparator, with the signature Vitest browser mode uses for `comparators`. */
|
|
30
|
+
export type Comparator<Options extends object = Record<string, unknown>> = (reference: RgbaImage, actual: RgbaImage, options: Options & {
|
|
31
|
+
createDiff: boolean;
|
|
32
|
+
}) => ComparatorResult;
|
|
33
|
+
/** Options of the default `pixelmatch` comparator, named as in Vitest browser mode. */
|
|
34
|
+
export type PixelmatchOptions = NonNullable<Parameters<typeof pixelmatch>[5]> & {
|
|
35
|
+
/** Fraction (0..1) of pixels allowed to differ. */
|
|
36
|
+
allowedMismatchedPixelRatio?: number;
|
|
37
|
+
/** Number of pixels allowed to differ. With both limits given the stricter wins; with neither, none may differ. */
|
|
38
|
+
allowedMismatchedPixels?: number;
|
|
39
|
+
};
|
|
40
|
+
/** Which comparator judges the images. `pixelmatch` (default) and `metrics` are built in; others come from `extendMatchers({ comparators })`. */
|
|
41
|
+
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>;
|
|
50
|
+
};
|
|
51
|
+
export type ScreenshotOptions = ComparatorSelection & {
|
|
52
|
+
/** Directory relative reference paths and diff images live in. Default: `__screenshots__` beside the test file. */
|
|
9
53
|
baselineDir?: string;
|
|
10
|
-
/**
|
|
11
|
-
threshold?: number;
|
|
12
|
-
/** Fraction (0..1) of pixels allowed to differ. Default 0.001. */
|
|
13
|
-
maxDiffRatio?: number;
|
|
14
|
-
/** Overwrite the baseline. Default: `UPDATE_SCREENSHOTS` env var is set. */
|
|
54
|
+
/** Overwrite the baseline file. Default: `UPDATE_SCREENSHOTS` env var is set. */
|
|
15
55
|
update?: boolean;
|
|
16
56
|
};
|
|
57
|
+
/** Defaults for every assertion plus custom comparators, like `test.browser.expect.toMatchScreenshot` in Vitest config. */
|
|
58
|
+
export type ScreenshotConfig = ComparatorSelection & {
|
|
59
|
+
comparators?: Record<string, Comparator<never>>;
|
|
60
|
+
};
|
|
17
61
|
declare module 'vitest' {
|
|
18
62
|
interface Assertion {
|
|
19
|
-
toMatchScreenshot(
|
|
63
|
+
toMatchScreenshot(reference: Reference, options?: ScreenshotOptions): Promise<void>;
|
|
20
64
|
}
|
|
21
65
|
}
|
|
22
|
-
|
|
66
|
+
/** `comparatorName: 'pixelmatch'` (default): Vitest browser mode's built-in comparison. */
|
|
67
|
+
export declare const pixelmatchComparator: Comparator<PixelmatchOptions>;
|
|
68
|
+
export declare function extendMatchers(config?: ScreenshotConfig): void;
|
|
23
69
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,98 @@
|
|
|
1
|
-
import { existsSync, mkdirSync
|
|
2
|
-
import { basename, dirname, join } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join } from 'node:path';
|
|
3
3
|
import pixelmatch from 'pixelmatch';
|
|
4
|
-
import
|
|
4
|
+
import sharp from 'sharp';
|
|
5
5
|
import { expect } from 'vitest';
|
|
6
|
+
import { metricsComparator } from './metrics.js';
|
|
6
7
|
const result = (pass, message) => ({ pass, message: () => message });
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const raw = ({ width, height, data }) => sharp(data, { raw: { width, height, channels: 4 } });
|
|
9
|
+
/** Decodes any format sharp reads (png, jpg, gif, webp, ...) into RGBA8. */
|
|
10
|
+
const readImage = async (file) => {
|
|
11
|
+
const { data, info } = await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
12
|
+
return { width: info.width, height: info.height, data };
|
|
11
13
|
};
|
|
12
|
-
|
|
14
|
+
/** Encodes by file extension; lossy formats (jpg, webp) alter pixels on write. */
|
|
15
|
+
const writeImage = (file, image) => {
|
|
16
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
17
|
+
return raw(image)
|
|
18
|
+
.toFormat(extname(file).slice(1))
|
|
19
|
+
.toFile(file);
|
|
20
|
+
};
|
|
21
|
+
const toRgba = async (source) => {
|
|
22
|
+
if ('data' in source)
|
|
23
|
+
return source;
|
|
24
|
+
if ('readPixels' in source)
|
|
25
|
+
return source.readPixels();
|
|
26
|
+
if ('getImageData' in source)
|
|
27
|
+
return source.getImageData();
|
|
28
|
+
await source.decode();
|
|
29
|
+
// node-webgl's Image and ImageBitmap expose decoded pixels this way (it is what their own texImage2D uses).
|
|
30
|
+
const rgba = source._toRGBA8?.();
|
|
31
|
+
if (!rgba)
|
|
32
|
+
throw new Error('unsupported image source: expected RGBA data, readPixels(), getImageData() or an Image');
|
|
33
|
+
return rgba;
|
|
34
|
+
};
|
|
35
|
+
/** `comparatorName: 'pixelmatch'` (default): Vitest browser mode's built-in comparison. */
|
|
36
|
+
export const pixelmatchComparator = (reference, actual, { createDiff, allowedMismatchedPixelRatio, allowedMismatchedPixels, ...pixelmatchOptions }) => {
|
|
37
|
+
const { width, height } = actual;
|
|
38
|
+
const total = width * height;
|
|
39
|
+
const diff = createDiff ? new Uint8Array(total * 4) : undefined;
|
|
40
|
+
const count = pixelmatch(reference.data, actual.data, diff, width, height, pixelmatchOptions);
|
|
41
|
+
const limits = [allowedMismatchedPixels, allowedMismatchedPixelRatio && allowedMismatchedPixelRatio * total];
|
|
42
|
+
const allowed = limits.some((l) => l !== undefined) ? Math.min(...limits.filter((l) => l !== undefined)) : 0;
|
|
43
|
+
return {
|
|
44
|
+
pass: count <= allowed,
|
|
45
|
+
diff: diff ? { width, height, data: diff } : null,
|
|
46
|
+
message: `${count} pixels (${((count / total) * 100).toFixed(3)}%) differ, ${Math.floor(allowed)} allowed`,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export function extendMatchers(config = {}) {
|
|
50
|
+
const comparators = {
|
|
51
|
+
pixelmatch: pixelmatchComparator,
|
|
52
|
+
metrics: metricsComparator,
|
|
53
|
+
...config.comparators,
|
|
54
|
+
};
|
|
13
55
|
expect.extend({
|
|
14
|
-
toMatchScreenshot(received,
|
|
15
|
-
const dir = options.baselineDir ?? join(dirname(this.testPath
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
async toMatchScreenshot(received, reference, options = {}) {
|
|
57
|
+
const dir = options.baselineDir ?? join(dirname(this.testPath), '__screenshots__');
|
|
58
|
+
const actual = await toRgba(received);
|
|
59
|
+
let file;
|
|
60
|
+
let baseline;
|
|
61
|
+
if (typeof reference === 'string') {
|
|
62
|
+
if (!extname(reference))
|
|
63
|
+
throw new Error(`reference "${reference}" needs an image extension, e.g. "${reference}.png"`);
|
|
64
|
+
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)) {
|
|
67
|
+
await writeImage(file, actual);
|
|
68
|
+
return result(true, `wrote baseline ${file}`);
|
|
69
|
+
}
|
|
70
|
+
if (!existsSync(file))
|
|
71
|
+
return result(false, `missing baseline ${file}; run with UPDATE_SCREENSHOTS=1`);
|
|
72
|
+
baseline = await readImage(file);
|
|
23
73
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
74
|
+
else {
|
|
75
|
+
baseline = await toRgba(reference);
|
|
76
|
+
}
|
|
77
|
+
const label = file ? basename(file) : this.currentTestName;
|
|
27
78
|
if (baseline.width !== actual.width || baseline.height !== actual.height) {
|
|
28
|
-
return result(false,
|
|
79
|
+
return result(false, `${label}: size mismatch: baseline ${baseline.width}x${baseline.height}, actual ${actual.width}x${actual.height}`);
|
|
29
80
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
81
|
+
const name = options.comparatorName ?? config.comparatorName ?? 'pixelmatch';
|
|
82
|
+
const comparator = comparators[name];
|
|
83
|
+
if (!comparator)
|
|
84
|
+
throw new Error(`unknown comparator "${name}"; known: ${Object.keys(comparators).join(', ')}`);
|
|
85
|
+
const inherited = name === (config.comparatorName ?? 'pixelmatch') ? config.comparatorOptions : undefined;
|
|
86
|
+
const { pass, diff, message } = comparator(baseline, actual, {
|
|
87
|
+
...inherited,
|
|
88
|
+
...options.comparatorOptions,
|
|
89
|
+
createDiff: true,
|
|
33
90
|
});
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
if (!pass)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return result(pass, `${basename(file)}: ${count} pixels (${(ratio * 100).toFixed(3)}%) differ; see ${name}.diff.png`);
|
|
91
|
+
const stem = join(dir, file ? basename(file, extname(file)) : label.replace(/[^\w-]+/g, '_'));
|
|
92
|
+
const seeDiff = !pass && diff ? `; see ${stem}.diff.png` : '';
|
|
93
|
+
if (!pass)
|
|
94
|
+
await Promise.all([writeImage(`${stem}.actual.png`, actual), diff && writeImage(`${stem}.diff.png`, diff)]);
|
|
95
|
+
return result(pass, `${label}: ${message ?? (pass ? 'matches' : 'differs')}${seeDiff}`);
|
|
41
96
|
},
|
|
42
97
|
});
|
|
43
98
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Comparator, RgbaImage } from './index.js';
|
|
2
|
+
/** ImageMagick `compare -metric` names. Upper bounds: AE PAE MAE MSE RMSE DSSIM. Lower bounds: PSNR SSIM. */
|
|
3
|
+
export type Metric = 'AE' | 'PAE' | 'MAE' | 'MSE' | 'RMSE' | 'PSNR' | 'SSIM' | 'DSSIM';
|
|
4
|
+
export type MetricsOptions = {
|
|
5
|
+
/** ImageMagick `-fuzz`: per-channel RMS colour distance (0..1) within which two pixels count as equal. Default 0. */
|
|
6
|
+
fuzz?: number;
|
|
7
|
+
/** Absolute error: pixels differing by more than `fuzz`. Default bound when no metric is given: 0. */
|
|
8
|
+
AE?: number;
|
|
9
|
+
/** Peak absolute error, 0..1. */
|
|
10
|
+
PAE?: number;
|
|
11
|
+
/** Mean absolute error, 0..1. */
|
|
12
|
+
MAE?: number;
|
|
13
|
+
/** Mean squared error, 0..1. */
|
|
14
|
+
MSE?: number;
|
|
15
|
+
/** Root mean squared error, 0..1. */
|
|
16
|
+
RMSE?: number;
|
|
17
|
+
/** Peak signal-to-noise ratio in dB (lower bound). `Infinity` for identical images. */
|
|
18
|
+
PSNR?: number;
|
|
19
|
+
/** Structural similarity, 1 = identical (lower bound). Gaussian 11x11 window, sigma 1.5, over RGB. */
|
|
20
|
+
SSIM?: number;
|
|
21
|
+
/** Structural dissimilarity, `(1 - SSIM) / 2`. */
|
|
22
|
+
DSSIM?: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* AE, PAE, MAE, MSE, RMSE and PSNR in one pass, matching `magick compare` for RGBA images: colour channels are
|
|
26
|
+
* alpha-premultiplied and all four channels are averaged. Also returns which pixels differ by more than `fuzz`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function distortion(a: RgbaImage, b: RgbaImage, fuzz: number): {
|
|
29
|
+
AE: number;
|
|
30
|
+
PAE: number;
|
|
31
|
+
MAE: number;
|
|
32
|
+
MSE: number;
|
|
33
|
+
RMSE: number;
|
|
34
|
+
PSNR: number;
|
|
35
|
+
mask: Uint8Array<ArrayBuffer>;
|
|
36
|
+
};
|
|
37
|
+
/** Mean SSIM over RGB (K1 0.01, K2 0.03), the textbook definition also used by ssim.js and Playwright. */
|
|
38
|
+
export declare function ssim(a: RgbaImage, b: RgbaImage): number;
|
|
39
|
+
/**
|
|
40
|
+
* `comparatorName: 'metrics'`: ImageMagick `compare -metric` bounds. Give any subset; all must pass. With no
|
|
41
|
+
* bound given, requires `AE: 0` (an exact match, like `compare -metric AE`).
|
|
42
|
+
*/
|
|
43
|
+
export declare const metricsComparator: Comparator<MetricsOptions>;
|
|
44
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,6GAA6G;AAC7G,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvF,MAAM,MAAM,cAAc,GAAG;IAC3B,qHAAqH;IACrH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqBF;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM;;;;;;;;EAgClE;AA+BD,0GAA0G;AAC1G,wBAAgB,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAmBvD;AAkBD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAAU,CAAC,cAAc,CAgCxD,CAAC"}
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const LOWER_IS_WORSE = new Set(['PSNR', 'SSIM']);
|
|
2
|
+
const METRICS = new Set(['AE', 'PAE', 'MAE', 'MSE', 'RMSE', 'PSNR', 'SSIM', 'DSSIM']);
|
|
3
|
+
/**
|
|
4
|
+
* ImageMagick's IsFuzzyEquivalencePixel: alpha must be within `fuzz`, then the colour distance, scaled by both
|
|
5
|
+
* alphas (a 4D cone: transparent pixels always match), must be within `fuzz` per channel RMS.
|
|
6
|
+
*/
|
|
7
|
+
const fuzzyEqual = (a, b, i, fuzz2) => {
|
|
8
|
+
const sa = a[i + 3] / 255;
|
|
9
|
+
const sb = b[i + 3] / 255;
|
|
10
|
+
const dAlpha = (sa - sb) ** 2;
|
|
11
|
+
if (dAlpha > fuzz2)
|
|
12
|
+
return false;
|
|
13
|
+
const scale = sa * sb;
|
|
14
|
+
if (scale <= Number.EPSILON)
|
|
15
|
+
return true;
|
|
16
|
+
let d = 3 * dAlpha;
|
|
17
|
+
for (let c = 0; c < 3; c++)
|
|
18
|
+
d += scale * ((a[i + c] - b[i + c]) / 255) ** 2;
|
|
19
|
+
return d <= 3 * fuzz2;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* AE, PAE, MAE, MSE, RMSE and PSNR in one pass, matching `magick compare` for RGBA images: colour channels are
|
|
23
|
+
* alpha-premultiplied and all four channels are averaged. Also returns which pixels differ by more than `fuzz`.
|
|
24
|
+
*/
|
|
25
|
+
export function distortion(a, b, fuzz) {
|
|
26
|
+
const n = a.width * a.height;
|
|
27
|
+
const mask = new Uint8Array(n);
|
|
28
|
+
const fuzz2 = fuzz * fuzz;
|
|
29
|
+
let AE = 0;
|
|
30
|
+
let PAE = 0;
|
|
31
|
+
let sumAbs = 0;
|
|
32
|
+
let sumSq = 0;
|
|
33
|
+
for (let p = 0, i = 0; p < n; p++, i += 4) {
|
|
34
|
+
if (!fuzzyEqual(a.data, b.data, i, fuzz2)) {
|
|
35
|
+
mask[p] = 1;
|
|
36
|
+
AE++;
|
|
37
|
+
}
|
|
38
|
+
const sa = a.data[i + 3] / 255;
|
|
39
|
+
const sb = b.data[i + 3] / 255;
|
|
40
|
+
for (let c = 0; c < 4; c++) {
|
|
41
|
+
const d = c === 3 ? Math.abs(sa - sb) : Math.abs((a.data[i + c] / 255) * sa - (b.data[i + c] / 255) * sb);
|
|
42
|
+
if (d > PAE)
|
|
43
|
+
PAE = d;
|
|
44
|
+
sumAbs += d;
|
|
45
|
+
sumSq += d * d;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const MSE = sumSq / (n * 4);
|
|
49
|
+
return {
|
|
50
|
+
AE,
|
|
51
|
+
PAE,
|
|
52
|
+
MAE: sumAbs / (n * 4),
|
|
53
|
+
MSE,
|
|
54
|
+
RMSE: Math.sqrt(MSE),
|
|
55
|
+
PSNR: MSE > 0 ? 10 * Math.log10(1 / MSE) : Infinity,
|
|
56
|
+
mask,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Gaussian taps for an 11x11 window with sigma 1.5, normalised to sum 1 (Wang et al. 2004). */
|
|
60
|
+
const KERNEL = (() => {
|
|
61
|
+
const k = Float64Array.from({ length: 11 }, (_, i) => Math.exp(-((i - 5) ** 2) / (2 * 1.5 * 1.5)));
|
|
62
|
+
const sum = k.reduce((s, v) => s + v, 0);
|
|
63
|
+
return k.map((v) => v / sum);
|
|
64
|
+
})();
|
|
65
|
+
/** Separable Gaussian blur of a 3-channel float image, clamping to edge pixels. */
|
|
66
|
+
const blur = (src, width, height) => {
|
|
67
|
+
const pass = (input, dx, dy) => {
|
|
68
|
+
const out = new Float32Array(input.length);
|
|
69
|
+
for (let y = 0; y < height; y++)
|
|
70
|
+
for (let x = 0; x < width; x++)
|
|
71
|
+
for (let c = 0; c < 3; c++) {
|
|
72
|
+
let s = 0;
|
|
73
|
+
for (let k = -5; k <= 5; k++) {
|
|
74
|
+
const sx = Math.min(width - 1, Math.max(0, x + k * dx));
|
|
75
|
+
const sy = Math.min(height - 1, Math.max(0, y + k * dy));
|
|
76
|
+
s += KERNEL[k + 5] * input[(sy * width + sx) * 3 + c];
|
|
77
|
+
}
|
|
78
|
+
out[(y * width + x) * 3 + c] = s;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
return pass(pass(src, 1, 0), 0, 1);
|
|
83
|
+
};
|
|
84
|
+
const mul = (x, y) => x.map((v, i) => v * y[i]);
|
|
85
|
+
/** Mean SSIM over RGB (K1 0.01, K2 0.03), the textbook definition also used by ssim.js and Playwright. */
|
|
86
|
+
export function ssim(a, b) {
|
|
87
|
+
const { width, height } = a;
|
|
88
|
+
const rgb = (img) => Float32Array.from({ length: width * height * 3 }, (_, i) => img.data[i + Math.floor(i / 3)] / 255);
|
|
89
|
+
const fa = rgb(a);
|
|
90
|
+
const fb = rgb(b);
|
|
91
|
+
const [ma, mb, maa, mbb, mab] = [fa, fb, mul(fa, fa), mul(fb, fb), mul(fa, fb)].map((img) => blur(img, width, height));
|
|
92
|
+
const c1 = 0.01 ** 2;
|
|
93
|
+
const c2 = 0.03 ** 2;
|
|
94
|
+
let sum = 0;
|
|
95
|
+
for (let i = 0; i < fa.length; i++) {
|
|
96
|
+
const va = maa[i] - ma[i] ** 2;
|
|
97
|
+
const vb = mbb[i] - mb[i] ** 2;
|
|
98
|
+
const cov = mab[i] - ma[i] * mb[i];
|
|
99
|
+
sum += ((2 * ma[i] * mb[i] + c1) * (2 * cov + c2)) / ((ma[i] ** 2 + mb[i] ** 2 + c1) * (va + vb + c2));
|
|
100
|
+
}
|
|
101
|
+
return sum / fa.length;
|
|
102
|
+
}
|
|
103
|
+
/** Diff image in pixelmatch's style: the actual image faded towards white, differing pixels in red. */
|
|
104
|
+
const diffImage = (actual, mask) => {
|
|
105
|
+
const data = new Uint8Array(actual.data.length);
|
|
106
|
+
for (let p = 0, i = 0; p < mask.length; p++, i += 4) {
|
|
107
|
+
if (mask[p])
|
|
108
|
+
data.set([255, 0, 0, 255], i);
|
|
109
|
+
else {
|
|
110
|
+
const grey = 255 - 0.1 * (255 - (0.299 * actual.data[i] + 0.587 * actual.data[i + 1] + 0.114 * actual.data[i + 2]));
|
|
111
|
+
data.set([grey, grey, grey, 255], i);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { width: actual.width, height: actual.height, data };
|
|
115
|
+
};
|
|
116
|
+
const fmt = (n) => (Number.isFinite(n) ? String(Number(n.toPrecision(4))) : String(n));
|
|
117
|
+
/**
|
|
118
|
+
* `comparatorName: 'metrics'`: ImageMagick `compare -metric` bounds. Give any subset; all must pass. With no
|
|
119
|
+
* bound given, requires `AE: 0` (an exact match, like `compare -metric AE`).
|
|
120
|
+
*/
|
|
121
|
+
export const metricsComparator = (reference, actual, { createDiff, fuzz = 0, ...bounds }) => {
|
|
122
|
+
for (const key of Object.keys(bounds)) {
|
|
123
|
+
if (!METRICS.has(key))
|
|
124
|
+
throw new Error(`unknown metric "${key}"; expected one of ${[...METRICS].join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
const wanted = Object.keys(bounds).filter((k) => bounds[k] !== undefined);
|
|
127
|
+
if (wanted.length === 0) {
|
|
128
|
+
bounds.AE = 0;
|
|
129
|
+
wanted.push('AE');
|
|
130
|
+
}
|
|
131
|
+
const values = distortion(reference, actual, fuzz);
|
|
132
|
+
if (wanted.includes('SSIM') || wanted.includes('DSSIM')) {
|
|
133
|
+
values.SSIM = ssim(reference, actual);
|
|
134
|
+
values.DSSIM = (1 - values.SSIM) / 2;
|
|
135
|
+
}
|
|
136
|
+
const failed = wanted.filter((k) => (LOWER_IS_WORSE.has(k) ? values[k] < bounds[k] : values[k] > bounds[k]));
|
|
137
|
+
const report = [...METRICS]
|
|
138
|
+
.filter((k) => values[k] !== undefined)
|
|
139
|
+
.map((k) => `${k} ${fmt(values[k])}${k === 'AE' ? ` (fuzz ${fmt(fuzz)})` : ''}`)
|
|
140
|
+
.join(', ');
|
|
141
|
+
const why = failed
|
|
142
|
+
.map((k) => `${k} ${fmt(values[k])} ${LOWER_IS_WORSE.has(k) ? '<' : '>'} ${fmt(bounds[k])}`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
return {
|
|
145
|
+
pass: failed.length === 0,
|
|
146
|
+
diff: createDiff ? diffImage(actual, values.mask) : null,
|
|
147
|
+
message: failed.length ? `${report}; failed: ${why}` : report,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AA0BA,MAAM,cAAc,GAAG,IAAI,GAAG,CAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AACzD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE9F;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,CAAoB,EAAE,CAAoB,EAAE,CAAS,EAAE,KAAa,EAAE,EAAE;IAC1F,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC;IAC3B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC;IAC3B,MAAM,MAAM,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,MAAM,GAAG,KAAK;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,KAAK,GAAG,EAAE,GAAG,EAAE,CAAC;IACtB,IAAI,KAAK,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9E,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;AACxB,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,CAAY,EAAE,CAAY,EAAE,IAAY;IACjE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACZ,EAAE,EAAE,CAAC;QACP,CAAC;QACD,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC;QAChC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YAC5G,IAAI,CAAC,GAAG,GAAG;gBAAE,GAAG,GAAG,CAAC,CAAC;YACrB,MAAM,IAAI,CAAC,CAAC;YACZ,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,OAAO;QACL,EAAE;QACF,GAAG;QACH,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACrB,GAAG;QACH,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ;QACnD,IAAI;KACL,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE;IACnB,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACnG,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;AAC/B,CAAC,CAAC,EAAE,CAAC;AAEL,mFAAmF;AACnF,MAAM,IAAI,GAAG,CAAC,GAAiB,EAAE,KAAa,EAAE,MAAc,EAAE,EAAE;IAChE,MAAM,IAAI,GAAG,CAAC,KAAmB,EAAE,EAAU,EAAE,EAAU,EAAE,EAAE;QAC3D,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE;YAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;gBAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC3B,IAAI,CAAC,GAAG,CAAC,CAAC;oBACV,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;wBAC7B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;wBACxD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;wBACzD,CAAC,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,KAAK,CAAC,CAAC,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,CAAC;oBAC1D,CAAC;oBACD,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;gBACnC,CAAC;QACL,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,MAAM,GAAG,GAAG,CAAC,CAAe,EAAE,CAAe,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;AAE7E,0GAA0G;AAC1G,MAAM,UAAU,IAAI,CAAC,CAAY,EAAE,CAAY;IAC7C,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,GAAG,GAAG,CAAC,GAAc,EAAE,EAAE,CAC7B,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,GAAG,GAAG,CAAC,CAAC;IACtG,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAClB,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC1F,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CACP,CAAC;IACpB,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC;IACrB,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,EAAE,GAAG,GAAI,CAAC,CAAC,CAAE,GAAG,EAAG,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC;QACnC,MAAM,EAAE,GAAG,GAAI,CAAC,CAAC,CAAE,GAAG,EAAG,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC;QACnC,MAAM,GAAG,GAAG,GAAI,CAAC,CAAC,CAAE,GAAG,EAAG,CAAC,CAAC,CAAE,GAAG,EAAG,CAAC,CAAC,CAAE,CAAC;QACzC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAE,GAAG,EAAG,CAAC,CAAC,CAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAG,CAAC,CAAC,CAAE,IAAI,CAAC,GAAG,EAAG,CAAC,CAAC,CAAE,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACjH,CAAC;IACD,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC;AACzB,CAAC;AAED,uGAAuG;AACvG,MAAM,SAAS,GAAG,CAAC,MAAiB,EAAE,IAAgB,EAAa,EAAE;IACnE,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,CAAC,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;aACtC,CAAC;YACJ,MAAM,IAAI,GACR,GAAG,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAE,GAAG,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC;YAC5G,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;AAC9D,CAAC,CAAC;AAEF,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/F;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA+B,CAC3D,SAAS,EACT,MAAM,EACN,EAAE,UAAU,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,MAAM,EAAE,EACnC,EAAE;IACF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAa,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,sBAAsB,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3F,CAAC;IACD,MAAM,MAAM,GAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IACxF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QACd,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,MAAM,MAAM,GAAoE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACpH,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACxD,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;IACjH,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;SAChF,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,GAAG,GAAG,MAAM;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;SAC7F,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QACzB,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;QACxD,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,aAAa,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM;KAC9D,CAAC;AACJ,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-screenshot",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "toMatchScreenshot(): pixel-diff
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
7
7
|
"pixelmatch",
|
|
@@ -42,15 +42,12 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"build": "tsc
|
|
46
|
-
"tsc": "tsc
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"tsc": "tsc"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"pixelmatch": "^7.1.0",
|
|
50
|
-
"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@types/pngjs": "^6.0.5"
|
|
50
|
+
"sharp": "^0.35.4"
|
|
54
51
|
},
|
|
55
52
|
"peerDependencies": {
|
|
56
53
|
"vitest": ">=3 <5"
|